diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 3d4db7260..69ff813f1 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -34,7 +34,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7] + python-version: ["3.10"] steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v3 @@ -53,7 +53,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7] + python-version: ["3.10"] steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v3 diff --git a/k8s/Chart.yaml b/k8s/Chart.yaml index e995e1d2e..aa7fe32d2 100644 --- a/k8s/Chart.yaml +++ b/k8s/Chart.yaml @@ -12,7 +12,7 @@ version: 0.8.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. -appVersion: 0.8.0 +appVersion: 0.11.0 # Chart dependencies dependencies: diff --git a/lifemonitor/app.py b/lifemonitor/app.py index edb8ceb13..08d220fdb 100644 --- a/lifemonitor/app.py +++ b/lifemonitor/app.py @@ -25,14 +25,15 @@ from flask import Flask, jsonify, redirect, render_template, request, url_for from flask_cors import CORS from flask_migrate import Migrate -from lifemonitor import redis import lifemonitor.config as config +from lifemonitor import redis from lifemonitor.auth.services import current_user from lifemonitor.integrations import init_integrations from lifemonitor.metrics import init_metrics from lifemonitor.routes import register_routes from lifemonitor.tasks import init_task_queues +from lifemonitor.utils import get_domain from . import commands from .cache import init_cache @@ -101,7 +102,7 @@ def health(): @app.route("/openapi.html") def openapi(): - return redirect('/static/apidocs.html') + return redirect('/static/specs/apidocs.html', code=302) @app.before_request def set_request_start_time(): @@ -110,6 +111,14 @@ def set_request_start_time(): @app.after_request def log_response(response): logger = logging.getLogger("response") + # logger.debug("Current user: %s", current_user) + # logger.debug("request: %s %s %s %s %s %s", + # request.remote_addr, request.method, request.path, + # request.scheme, request.full_path, request.referrer, + # ) + # for h in request.headers: + # logger.debug("header: %s %s", h, request.headers.get(h, None)) + # log the request processing_time = (time.time() * 1000.0 - request.start_time * 1000.0) logger.info( "resp: %s %s %s %s %s %s %s %s %0.3fms", @@ -123,6 +132,7 @@ def log_response(response): request.user_agent, processing_time ) + # return the response return response return app @@ -158,3 +168,5 @@ def initialize_app(app: Flask, app_context, prom_registry=None, load_jobs: bool init_metrics(app, prom_registry) # register commands commands.register_commands(app) + # register the domain filter with Jinja + app.jinja_env.filters['domain'] = get_domain diff --git a/lifemonitor/auth/__init__.py b/lifemonitor/auth/__init__.py index 385ed74a3..c6b66cfd1 100644 --- a/lifemonitor/auth/__init__.py +++ b/lifemonitor/auth/__init__.py @@ -22,7 +22,7 @@ import lifemonitor.auth.oauth2 as oauth2 -from .controllers import blueprint as auth_blueprint +from .controllers import blueprint as auth_blueprint, CustomSessionInterface from .models import EventType, Notification, User, UserNotification from .services import (NotAuthorizedException, authorized, current_registry, current_user, login_manager, login_registry, login_user, @@ -37,6 +37,7 @@ def register_api(app, specs_dir): oauth2.client.register_api(app, specs_dir, "auth.merge") oauth2.server.register_api(app, specs_dir) app.register_blueprint(auth_blueprint) + app.session_interface = CustomSessionInterface() login_manager.init_app(app) diff --git a/lifemonitor/auth/controllers.py b/lifemonitor/auth/controllers.py index dfa49a652..9dfef441d 100644 --- a/lifemonitor/auth/controllers.py +++ b/lifemonitor/auth/controllers.py @@ -24,6 +24,7 @@ import flask from flask import (current_app, flash, redirect, render_template, request, session, url_for) +from flask.sessions import SecureCookieSessionInterface from flask_login import login_required, login_user, logout_user from lifemonitor.cache import Timeout, cached, clear_cache @@ -162,7 +163,7 @@ def get_registry_user(user_id): @blueprint.route("/", methods=("GET",)) def index(): - return redirect(url_for('auth.profile', back=request.args.get('back', False))) + return redirect(url_for('auth.profile', back=request.args.get('back', None))) @blueprint.route("/profile", methods=("GET",)) @@ -177,7 +178,8 @@ def profile(form=None, passwordForm=None, currentView=None, logger.debug("Pushing back param to session") else: logger.debug("Getting back param from session") - back_param = back_param or session.get('lm_back_param', False) + back_param = back_param or session.get('lm_back_param', None) + session['lm_back_param'] = back_param logger.debug("detected back param: %s", back_param) from lifemonitor.api.models.registries.forms import RegistrySettingsForm from lifemonitor.integrations.github.forms import GithubSettingsForm @@ -212,7 +214,7 @@ def register(): login_user(user) flash("Account created", category="success") clear_cache() - return redirect(url_for("auth.index")) + return redirect(url_for("auth.profile")) return render_template("auth/register.j2", form=form, action=url_for('auth.register'), providers=get_providers(), is_service_available=is_service_alive) @@ -228,7 +230,6 @@ def identity_not_found(): form = RegisterForm() user = identity.user # workaround to force clean DB session - from lifemonitor.db import db db.session.rollback() return render_template("auth/identity_not_found.j2", form=form, action=url_for('auth.register_identity') if flask.session.get('sign_in', False) else url_for('auth.register'), @@ -242,7 +243,7 @@ def register_identity(): logger.debug("Current provider identity: %r", identity) if not identity: flash("Unable to register the user") - flask.abort(400) + return redirect(url_for("auth.register")) logger.debug("Provider identity on session: %r", identity) logger.debug("User Info: %r", identity.user_info) user = identity.user @@ -254,7 +255,7 @@ def register_identity(): flash("Account created", category="success") clear_cache() return redirect(url_for("auth.index")) - return render_template("auth/register.j2", form=form, action=url_for('auth.register'), + return render_template("auth/register.j2", form=form, action=url_for('auth.register_identity'), identity=identity, user=user, providers=get_providers()) @@ -272,6 +273,7 @@ def login(): session.pop('_flashes', None) flash("You have logged in", category="success") return redirect(NextRouteRegistry.pop(url_for("auth.profile"))) + flask.session['lm_back_param'] = flask.request.args.get('back', None) return render_template("auth/login.j2", form=form, providers=get_providers(), is_service_available=is_service_alive) @@ -281,9 +283,10 @@ def login(): def logout(): logout_user() session.pop('_flashes', None) + back_param = session.pop('lm_back_param', None) flash("You have logged out", category="success") NextRouteRegistry.clear() - next_route = request.args.get('next', '/') + next_route = request.args.get('next', '/logout' if back_param else '/') logger.debug("Next route after logout: %r", next_route) return redirect(next_route) @@ -606,3 +609,16 @@ def delete_generic_code_flow_client(): flash("App removed!", category="success") clear_cache() return redirect(url_for('auth.profile', currentView='oauth2ClientsTab')) + + +class CustomSessionInterface(SecureCookieSessionInterface): + """Prevent creating session from API requests.""" + + def save_session(self, *args, **kwargs): + + if flask.g.get('login_via_request'): + logger.debug("Prevent creating session from API requests") + return + # if login_via_request is not set, then create a new session + logger.debug("Saving session") + return super(CustomSessionInterface, self).save_session(*args, **kwargs) diff --git a/lifemonitor/auth/forms.py b/lifemonitor/auth/forms.py index 18794a9dd..46df3418f 100644 --- a/lifemonitor/auth/forms.py +++ b/lifemonitor/auth/forms.py @@ -84,7 +84,8 @@ def create_user(self, identity=None): db.session.commit() return user except IntegrityError as e: - logger.debug(e) + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) self.username.errors.append("This username is already taken") db.session.rollback() return None diff --git a/lifemonitor/auth/oauth2/client/controllers.py b/lifemonitor/auth/oauth2/client/controllers.py index 9ba70b8fb..3734f456d 100644 --- a/lifemonitor/auth/oauth2/client/controllers.py +++ b/lifemonitor/auth/oauth2/client/controllers.py @@ -135,7 +135,7 @@ def logout(name: str): # Determine the right next hop next_url = NextRouteRegistry.pop() return redirect(next_url, code=307) if next_url \ - else RequestHelper.response() or redirect('/', code=302) + else RequestHelper.response() or redirect('/account', code=302) return blueprint @@ -261,7 +261,7 @@ def handle_authorize(self, provider: FlaskRemoteApp, token, user_info: OAuthUser next_url = NextRouteRegistry.pop() flash(f"Logged with your \"{identity.provider.name}\" identity.", category="success") return redirect(next_url, code=307) if next_url \ - else RequestHelper.response() or redirect('/', code=302) + else RequestHelper.response() or redirect('/account', code=302) @staticmethod def validate_identity_token(identity: OAuthIdentity): diff --git a/lifemonitor/auth/services.py b/lifemonitor/auth/services.py index 753dd9cd8..3ed1cee34 100644 --- a/lifemonitor/auth/services.py +++ b/lifemonitor/auth/services.py @@ -24,11 +24,12 @@ from functools import wraps import flask_login -from flask import g, request, url_for +from flask import current_app, g, request, url_for +from werkzeug.local import LocalProxy + from lifemonitor.auth.models import Anonymous, ApiKey, User from lifemonitor.exceptions import LifeMonitorException from lifemonitor.lang import messages -from werkzeug.local import LocalProxy # Config a module level logger logger = logging.getLogger(__name__) @@ -66,8 +67,24 @@ def load_user_from_header(_req): return None -def login_user(user): - flask_login.login_user(user) +@flask_login.user_loaded_from_request.connect +def user_loaded_from_request(app, user=None): + logger.debug("User loaded from request: %s", user) + g.login_via_request = True + + +def login_user(user, remember=False, duration=None, force=False, fresh=True): + logger.debug("User logged in: %s", user) + logger.debug("g.get('login_via_request'): %r", g.get('login_via_request')) + + # signal if API key is provided or Token is in the request header + if request.headers.get('ApiKey', None) or request.headers.get('Authorization', None): + flask_login.user_loaded_from_request.send(current_app._get_current_object(), user=user) + logger.debug("g.get('login_via_request'): %r", g.get('login_via_request')) + else: + logger.debug("Not logged in via request") + + flask_login.login_user(user, remember=remember, duration=duration, force=force, fresh=fresh) def logout_user(): @@ -158,3 +175,23 @@ def check_api_key(api_key, required_scopes): login_user(api_key.user) # return the user_id return {'uid': api_key.user.id} + + +def check_cookie(cookie, required_scopes): + logger.debug("Checking the cookie: %r; scopes required: %r", cookie, required_scopes) + logger.debug("Current user: %r", current_user) + + # check is an ApiKey is present in the request + if request.headers.get('ApiKey', None): + return check_api_key(request.headers.get('ApiKey', None), required_scopes) + + # check is an Authorization header is present in the request + if request.headers.get('Authorization', None): + from lifemonitor.auth.oauth2.server.services import get_token_scopes + auth_header = request.headers.get('Authorization', None) + if auth_header.startswith('Bearer '): + token = auth_header.replace('Bearer ', '') + return get_token_scopes(token) + + # if the cookie is present, return the user_id + return {'uid': current_user.id} diff --git a/lifemonitor/config.py b/lifemonitor/config.py index 8bde89049..e2469f9f9 100644 --- a/lifemonitor/config.py +++ b/lifemonitor/config.py @@ -128,6 +128,8 @@ class BaseConfig: ENABLE_REGISTRY_INTEGRATION = False # Service Availability Timeout SERVICE_AVAILABILITY_TIMEOUT = 1 + # Cookie Settings + SESSION_COOKIE_NAME = 'lifemonitor_session' class DevelopmentConfig(BaseConfig): diff --git a/lifemonitor/static/api.yaml b/lifemonitor/static/api.yaml deleted file mode 120000 index a97f32b16..000000000 --- a/lifemonitor/static/api.yaml +++ /dev/null @@ -1 +0,0 @@ -../../specs/api.yaml \ No newline at end of file diff --git a/lifemonitor/static/specs/api.yaml b/lifemonitor/static/specs/api.yaml new file mode 120000 index 000000000..500e0b8fa --- /dev/null +++ b/lifemonitor/static/specs/api.yaml @@ -0,0 +1 @@ +../../../specs/api.yaml \ No newline at end of file diff --git a/lifemonitor/static/apidocs.html b/lifemonitor/static/specs/apidocs.html similarity index 97% rename from lifemonitor/static/apidocs.html rename to lifemonitor/static/specs/apidocs.html index 94bce3236..e65f27322 100644 --- a/lifemonitor/static/apidocs.html +++ b/lifemonitor/static/specs/apidocs.html @@ -42,6 +42,7 @@ primary-color="#1f8787" theme="light" schema-description-expanded="false" + fetch-credentials="omit" >
- + + + + {% endblock stylesheets %} @@ -79,8 +82,10 @@ - + + + {# Enable notifications #} {{ macros.messages() }} @@ -88,6 +93,14 @@ {% endblock javascripts_libraries %} {% block javascripts %} {% endblock javascripts %} + + + {% if config.EXTERNAL_SERVER_URL %} + {% set domain = config.EXTERNAL_SERVER_URL|domain %} + + {% endif %} diff --git a/lifemonitor/utils.py b/lifemonitor/utils.py index 7ec310ba7..ab9235031 100644 --- a/lifemonitor/utils.py +++ b/lifemonitor/utils.py @@ -41,6 +41,7 @@ from importlib import import_module from os.path import basename, dirname, isfile, join from typing import Dict, List, Literal, Optional, Tuple, Type +from urllib.parse import urlparse import flask import networkx as nx @@ -612,6 +613,13 @@ def extract_zip(archive_path, target_path=None): raise lm_exceptions.NotValidROCrateException(detail=msg, original_error=str(e)) +def get_domain(value): + try: + return urlparse(value).netloc.split(':')[0] + except Exception: + raise ValueError("Invalid URL: %r" % value) + + def _make_git_credentials_callback(token: str = None): return pygit2.RemoteCallbacks(pygit2.UserPass('x-access-token', token)) if token else None diff --git a/specs/api.yaml b/specs/api.yaml index f70913958..7b300bdb4 100644 --- a/specs/api.yaml +++ b/specs/api.yaml @@ -3,7 +3,7 @@ openapi: "3.0.0" info: - version: "0.10.0" + version: "0.11.0" title: "Life Monitor API" description: | *Workflow sustainability service* @@ -18,7 +18,7 @@ info: servers: - url: / description: > - Version 0.10.0 of API. + Version 0.11.0 of API. tags: - name: GitHub Integration @@ -166,6 +166,7 @@ paths: summary: "Get registry index" description: "Get the index of workflows available on the registry" security: + - cookieAuth: [] - apiKey: ["user.workflow.read"] - RegistryCodeFlow: ["user.workflow.read"] - AuthorizationCodeFlow: ["user.workflow.read"] @@ -195,6 +196,7 @@ paths: summary: "Get registry index workflow" description: "Get information about the specified workflow of the registry index" security: + - cookieAuth: [] - apiKey: ["user.workflow.read"] - RegistryCodeFlow: ["user.workflow.read"] - AuthorizationCodeFlow: ["user.workflow.read"] @@ -226,6 +228,7 @@ paths: description: | Get information about the current (authenticated) user security: + - cookieAuth: [] - apiKey: ["user.profile"] - RegistryCodeFlow: ["user.profile"] - AuthorizationCodeFlow: ["user.profile"] @@ -254,6 +257,7 @@ paths: description: | List all notifications for the current (authenticated) user security: + - cookieAuth: [] - apiKey: ["user.profile"] - RegistryCodeFlow: ["user.profile"] - AuthorizationCodeFlow: ["user.profile"] @@ -281,6 +285,7 @@ paths: description: | Mark as read notifications for the current (authenticated) user security: + - cookieAuth: [] - apiKey: ["user.profile"] - RegistryCodeFlow: ["user.profile"] - AuthorizationCodeFlow: ["user.profile"] @@ -310,6 +315,7 @@ paths: description: | Delete notifications for the current (authenticated) user security: + - cookieAuth: [] - apiKey: ["user.profile"] - RegistryCodeFlow: ["user.profile"] - AuthorizationCodeFlow: ["user.profile"] @@ -344,6 +350,7 @@ paths: description: | Delete notification for the current (authenticated) user security: + - cookieAuth: [] - apiKey: ["user.profile"] - RegistryCodeFlow: ["user.profile"] - AuthorizationCodeFlow: ["user.profile"] @@ -519,6 +526,7 @@ paths: Submit (a new version of) a workflow indexed on the specified registry as the current (authenticated) user security: + - cookieAuth: [] - apiKey: ["user.workflow.write"] - AuthorizationCodeFlow: ["user.workflow.write"] parameters: @@ -615,6 +623,7 @@ paths: description: | List all workflows submitted or subscribed by the current user security: + - cookieAuth: [] - apiKey: ["user.workflow.read"] - RegistryCodeFlow: ["user.workflow.read"] - AuthorizationCodeFlow: ["user.workflow.read"] @@ -640,6 +649,7 @@ paths: description: | Submit (a new version of) a workflow. security: + - cookieAuth: [] - apiKey: ["user.workflow.write"] - AuthorizationCodeFlow: ["user.workflow.write"] requestBody: @@ -673,6 +683,7 @@ paths: description: | List all subscriptions for the current user security: + - cookieAuth: [] - apiKey: ["user.workflow.read"] - RegistryCodeFlow: ["user.workflow.read"] - AuthorizationCodeFlow: ["user.workflow.read"] @@ -695,6 +706,7 @@ paths: description: | List all workflows either public or registered by the current (authenticated) user or registry client. security: + - cookieAuth: [] - apiKey: ["workflow.read"] - RegistryClientCredentials: ["workflow.read"] - RegistryCodeFlow: ["workflow.read"] @@ -777,6 +789,7 @@ paths: operationId: "workflows_get_latest_version_by_id" tags: ["Workflows"] security: + - cookieAuth: [] - apiKey: ["workflow.read"] - AuthorizationCodeFlow: ["workflow.read"] - RegistryClientCredentials: ["workflow.read"] @@ -813,6 +826,7 @@ paths: operationId: "workflows_put" tags: ["Workflows"] security: + - cookieAuth: [] - apiKey: ["workflow.read"] - AuthorizationCodeFlow: ["workflow.read"] - RegistryClientCredentials: ["workflow.read"] @@ -844,6 +858,7 @@ paths: description: "Delete the specified workflow and all its versions" tags: ["Workflows"] security: + - cookieAuth: [] - apiKey: ["workflow.write"] - RegistryClientCredentials: ["workflow.write"] - RegistryCodeFlow: ["workflow.write"] @@ -870,6 +885,7 @@ paths: operationId: "user_workflow_subscribe" tags: ["Users"] security: + - cookieAuth: [] - apiKey: ["workflow.write"] - AuthorizationCodeFlow: ["workflow.write"] - RegistryCodeFlow: ["workflow.write"] @@ -898,6 +914,7 @@ paths: operationId: "user_workflow_subscribe_events" tags: ["Users"] security: + - cookieAuth: [] - apiKey: ["workflow.write"] - AuthorizationCodeFlow: ["workflow.write"] - RegistryCodeFlow: ["workflow.write"] @@ -935,6 +952,7 @@ paths: operationId: "user_workflow_unsubscribe" tags: ["Users"] security: + - cookieAuth: [] - apiKey: ["workflow.write"] - AuthorizationCodeFlow: ["workflow.write"] - RegistryCodeFlow: ["workflow.write"] @@ -960,6 +978,7 @@ paths: description: "List all versions of the specified workflow" tags: ["Workflows"] security: + - cookieAuth: [] - apiKey: ["workflow.read"] - RegistryClientCredentials: ["workflow.read"] - RegistryCodeFlow: ["workflow.read"] @@ -1017,6 +1036,7 @@ paths: operationId: "workflows_get_version_by_id" tags: ["Workflows"] security: + - cookieAuth: [] - apiKey: ["workflow.read"] - AuthorizationCodeFlow: ["workflow.read"] - RegistryClientCredentials: ["workflow.read"] @@ -1049,6 +1069,7 @@ paths: operationId: "workflows_version_put" tags: ["Workflows"] security: + - cookieAuth: [] - apiKey: ["workflow.read"] - AuthorizationCodeFlow: ["workflow.read"] - RegistryClientCredentials: ["workflow.read"] @@ -1084,6 +1105,7 @@ paths: description: "Delete the specified version of the specified workflow" tags: ["Workflows"] security: + - cookieAuth: [] - apiKey: ["workflow.write"] - RegistryClientCredentials: ["workflow.write"] - RegistryCodeFlow: ["workflow.write"] @@ -1111,6 +1133,7 @@ paths: description: "Get the test status for the specified workflow and workflow version" tags: ["Workflows"] security: + - cookieAuth: [] - apiKey: ["workflow.read"] - RegistryClientCredentials: ["workflow.read"] - RegistryCodeFlow: ["workflow.read"] @@ -1143,6 +1166,7 @@ paths: description: "Get the RO-Crate JSON metadata for the specified workflow and workflow version" tags: ["Workflows"] security: + - cookieAuth: [] - apiKey: ["workflow.read"] - RegistryClientCredentials: ["workflow.read"] - RegistryCodeFlow: ["workflow.read"] @@ -1173,6 +1197,7 @@ paths: description: "Get the RO-Crate ZIP archive for the specified workflow and workflow version" tags: ["Workflows"] security: + - cookieAuth: [] - apiKey: ["workflow.read"] - RegistryClientCredentials: ["workflow.read"] - RegistryCodeFlow: ["workflow.read"] @@ -1206,6 +1231,7 @@ paths: operationId: "workflows_get_suites" tags: ["Workflows"] security: + - cookieAuth: [] - apiKey: ["workflow.read"] - RegistryClientCredentials: ["workflow.read"] - RegistryCodeFlow: ["workflow.read"] @@ -1240,6 +1266,7 @@ paths: operationId: "suites_get_by_uuid" tags: ["Test Suites"] security: + - cookieAuth: [] - apiKey: ["workflow.read"] - RegistryClientCredentials: ["workflow.read"] - RegistryCodeFlow: ["workflow.read"] @@ -1272,6 +1299,7 @@ paths: operationId: "suites_put" tags: ["Test Suites"] security: + - cookieAuth: [] - apiKey: ["workflow.write"] - AuthorizationCodeFlow: ["workflow.write"] - RegistryClientCredentials: ["workflow.write"] @@ -1309,6 +1337,7 @@ paths: operationId: "suites_get_status" tags: ["Test Suites"] security: + - cookieAuth: [] - apiKey: ["workflow.read"] - RegistryClientCredentials: ["workflow.read"] - RegistryCodeFlow: ["workflow.read"] @@ -1336,6 +1365,7 @@ paths: operationId: "suites_get_instances" tags: ["Test Suites"] security: + - cookieAuth: [] - apiKey: ["workflow.read"] - RegistryClientCredentials: ["workflow.read"] - RegistryCodeFlow: ["workflow.read"] @@ -1365,6 +1395,7 @@ paths: operationId: "suites_post_instance" tags: ["Test Suites"] security: + - cookieAuth: [] - apiKey: ["workflow.write"] - RegistryClientCredentials: ["workflow.write"] - RegistryCodeFlow: ["workflow.write"] @@ -1419,6 +1450,7 @@ paths: operationId: "instances_get_by_id" tags: ["Test Instances"] security: + - cookieAuth: [] - apiKey: ["workflow.read"] - RegistryClientCredentials: ["workflow.read"] - RegistryCodeFlow: ["workflow.read"] @@ -1449,6 +1481,7 @@ paths: operationId: "instances_put" tags: ["Test Instances"] security: + - cookieAuth: [] - apiKey: ["workflow.write"] - AuthorizationCodeFlow: ["workflow.write"] - RegistryClientCredentials: ["workflow.write"] @@ -1485,6 +1518,7 @@ paths: description: "Delete the specified test instance" tags: ["Test Instances"] security: + - cookieAuth: [] - apiKey: ["workflow.write"] - RegistryClientCredentials: ["workflow.write"] - RegistryCodeFlow: ["workflow.write"] @@ -1511,6 +1545,7 @@ paths: operationId: "instances_get_builds" tags: ["Test Instances"] security: + - cookieAuth: [] - apiKey: ["workflow.read"] - RegistryClientCredentials: ["workflow.read"] - RegistryCodeFlow: ["workflow.read"] @@ -1543,6 +1578,7 @@ paths: operationId: "instances_builds_get_by_id" tags: ["Test Instances"] security: + - cookieAuth: [] - apiKey: ["workflow.read"] - RegistryClientCredentials: ["workflow.read"] - RegistryCodeFlow: ["workflow.read"] @@ -2882,6 +2918,12 @@ components: - status securitySchemes: + cookieAuth: + type: apiKey + in: cookie + name: lifemonitor_session # cookie name + x-apikeyInfoFunc: lifemonitor.auth.services.check_cookie + apiKey: type: apiKey in: header diff --git a/tests/test_users.py b/tests/test_users.py index a84644e03..9900ee08d 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -20,8 +20,10 @@ import logging import pytest +import requests from flask import g +from lifemonitor.utils import get_base_url from .conftest import ClientAuthenticationMethod from .utils import assert_properties_exist @@ -65,3 +67,30 @@ def test_user1_auth(user1, client_auth_method, user1_auth): assert "ApiKey" in user1_auth else: assert "Bearer" in user1_auth['Authorization'] + + +@pytest.mark.parametrize("client_auth_method", [ + ClientAuthenticationMethod.NOAUTH, + ClientAuthenticationMethod.BASIC, + ClientAuthenticationMethod.API_KEY, + ClientAuthenticationMethod.AUTHORIZATION_CODE +], indirect=True) +def test_user_auto_logout(user1, client_auth_method, user1_auth): + logger.debug("Auth: %r, %r, %r", user1_auth, client_auth_method, ClientAuthenticationMethod.BASIC.value) + + app_client = requests.session() + app_client_url = f'{get_base_url()}/users/current' + logger.debug("client URL: %r", app_client_url) + + r1 = app_client.get(app_client_url, headers=user1_auth) + logger.debug("headers: %r", r1.headers) + logger.debug("response: %r", r1.content) + if client_auth_method in [ClientAuthenticationMethod.NOAUTH, ClientAuthenticationMethod.BASIC]: + assert r1.status_code == 401, "Expected 401 status code" + else: + assert r1.status_code == 200, "Expected 200 status code" + logger.debug("Response R1: %r", r1.json) + + r2 = app_client.get(app_client_url) + logger.debug("headers: %r", r2.headers) + assert r2.status_code == 401, "Expected 401 status code"