From 711a1caf921a53daffe786ab962856371a773d0e Mon Sep 17 00:00:00 2001 From: Timo Nurminen Date: Wed, 21 Jun 2023 13:07:43 +0300 Subject: [PATCH] [#227] Implement issue, python to 3.10 --- .github/workflows/docs.yml | 2 +- .github/workflows/int.yml | 2 +- .github/workflows/style.yml | 2 +- .github/workflows/unit.yml | 4 +-- Dockerfile | 6 ++-- README.md | 2 +- beacon_api/app.py | 4 +-- beacon_api/conf/__init__.py | 3 +- beacon_api/conf/config.ini | 4 +++ beacon_api/permissions/ga4gh.py | 8 ++++- beacon_api/utils/logging.py | 2 +- docs/instructions.rst | 2 +- readthedocs.yml | 2 +- setup.py | 3 +- tests/test.ini | 4 +++ tests/test_app.py | 2 +- tests/test_basic.py | 56 ++++++++++++++++++++++++++++++++- tox.ini | 4 +-- 18 files changed, 90 insertions(+), 22 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a1eac033..597fba5c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -21,7 +21,7 @@ jobs: max-parallel: 4 matrix: os: [ubuntu-latest] - python-version: [3.8] + python-version: [3.10] runs-on: ${{ matrix.os }} diff --git a/.github/workflows/int.yml b/.github/workflows/int.yml index 92962db3..bbc29f06 100644 --- a/.github/workflows/int.yml +++ b/.github/workflows/int.yml @@ -9,7 +9,7 @@ jobs: max-parallel: 4 matrix: os: [ubuntu-latest] - python-version: [3.8] + python-version: [3.10] runs-on: ${{ matrix.os }} diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index 78942b73..a8ce307f 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -8,7 +8,7 @@ jobs: max-parallel: 4 matrix: os: [ubuntu-latest] - python-version: [3.8] + python-version: [3.10] runs-on: ${{ matrix.os }} diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 5ff1cb57..ffd9d081 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -11,7 +11,7 @@ jobs: max-parallel: 4 matrix: os: [ubuntu-latest] - python-version: [3.8] + python-version: [3.10] runs-on: ${{ matrix.os }} @@ -29,7 +29,7 @@ jobs: run: | python -m pip install --upgrade pip pip install tox tox-gh-actions coverage - - name: Run unit tests for python 3.8 + - name: Run unit tests for python 3.10 run: | tox -e unit_tests coverage lcov -o lcov.info diff --git a/Dockerfile b/Dockerfile index 0e4f4057..e54d10aa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8-alpine3.15 as BUILD +FROM python:3.10.12-alpine3.18 as BUILD RUN apk add --update \ && apk add --no-cache build-base curl-dev linux-headers bash git musl-dev\ @@ -18,7 +18,7 @@ COPY setup.py /root/beacon/setup.py COPY beacon_api /root/beacon/beacon_api RUN pip install /root/beacon -FROM python:3.8-alpine3.15 +FROM python:3.10.12-alpine3.18 RUN apk add --no-cache --update bash @@ -29,7 +29,7 @@ LABEL org.label-schema.vcs-url="https://github.com/CSCFI/beacon-python" RUN apk add --update \ && apk add --no-cache curl bzip2 xz -COPY --from=BUILD usr/local/lib/python3.8/ usr/local/lib/python3.8/ +COPY --from=BUILD usr/local/lib/python3.10/ usr/local/lib/python3.10/ COPY --from=BUILD /usr/local/bin/gunicorn /usr/local/bin/ diff --git a/README.md b/README.md index 2f86b872..6130dd38 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Documentation: https://beacon-python.readthedocs.io ### Quick start `beacon-python` Web Server requires: -* Python 3.8+; +* Python 3.10+; * running DB [PostgreSQL Server](https://www.postgresql.org/) 9.6+ (recommended 13). ```shell diff --git a/beacon_api/app.py b/beacon_api/app.py index 357e5179..977448b1 100644 --- a/beacon_api/app.py +++ b/beacon_api/app.py @@ -135,7 +135,7 @@ def main(): if __name__ == "__main__": - if sys.version_info < (3, 8): - LOG.error("beacon-python requires python 3.8") + if sys.version_info < (3, 10): + LOG.error("beacon-python requires python 3.10") sys.exit(1) main() diff --git a/beacon_api/conf/__init__.py b/beacon_api/conf/__init__.py index 6b02a8cd..fba5bbd7 100644 --- a/beacon_api/conf/__init__.py +++ b/beacon_api/conf/__init__.py @@ -63,13 +63,14 @@ def parse_oauth2_config_file(path: str) -> Any: """Parse configuration file.""" config = ConfigParser() config.read(path) - config_vars: Dict[str, Union[str, bool, None]] = { + config_vars: Dict[str, Union[str, bool, None, List[str]]] = { "server": config.get("oauth2", "server"), "issuers": config.get("oauth2", "issuers"), "userinfo": config.get("oauth2", "userinfo"), "audience": config.get("oauth2", "audience") or None, "verify_aud": bool(strtobool(config.get("oauth2", "verify_aud"))), "bona_fide_value": config.get("oauth2", "bona_fide_value"), + "trusted_jkus": config.get("oauth2", "trusted_jkus", fallback="").split(","), } return convert(config_vars) diff --git a/beacon_api/conf/config.ini b/beacon_api/conf/config.ini index c4a715f2..ae1ebe3c 100644 --- a/beacon_api/conf/config.ini +++ b/beacon_api/conf/config.ini @@ -122,3 +122,7 @@ audience= # If your service is not part of any network or AAI, but you still want to use tokens # produced by other AAI parties, set this value to False to skip the audience validation step verify_aud=False + +# Comma separated list of trusted JKUs +# JTW with an untrusted JKU will be denied access +trusted_jkus=http://test.csc.fi/jwkboofar diff --git a/beacon_api/permissions/ga4gh.py b/beacon_api/permissions/ga4gh.py index 4ec6def6..b1555d49 100644 --- a/beacon_api/permissions/ga4gh.py +++ b/beacon_api/permissions/ga4gh.py @@ -156,13 +156,19 @@ async def get_ga4gh_permissions(token: str) -> Tuple[set, bool]: # Get encoded passports (assorted JWTs) from /userinfo encoded_passports = await retrieve_user_data(token) - # Pre-process the assorted passports (JWTs) for dataset permissions and bona fide status parsing if encoded_passports: # Decode the passports and check their type for encoded_passport in encoded_passports: # Decode passport header, payload = await decode_passport(encoded_passport) + # If trusted_jkus variable is set, only allow passports with a trusted JKU + if not OAUTH2_CONFIG.trusted_jkus == [""]: + # Skip passports with untrusted JKUs + passport_jku = header.get("jku") + if passport_jku not in OAUTH2_CONFIG.trusted_jkus: + LOG.debug("Untrusted JKU.") + continue # Sort passports that carry dataset permissions pass_type = payload.get("ga4gh_visa_v1", {}).get("type") if pass_type == "ControlledAccessGrants": # nosec diff --git a/beacon_api/utils/logging.py b/beacon_api/utils/logging.py index 983ddee7..02251461 100644 --- a/beacon_api/utils/logging.py +++ b/beacon_api/utils/logging.py @@ -5,6 +5,6 @@ # Keeping it simple with the logging formatting formatting = "[%(asctime)s][%(name)s][%(process)d %(processName)s][%(levelname)-8s] (L:%(lineno)s) %(module)s | %(funcName)s: %(message)s" -logging.basicConfig(level=logging.INFO, format=formatting) +logging.basicConfig(level=logging.DEBUG, format=formatting) LOG = logging.getLogger("beacon") diff --git a/docs/instructions.rst b/docs/instructions.rst index 54e5f8fa..7e829c65 100644 --- a/docs/instructions.rst +++ b/docs/instructions.rst @@ -3,7 +3,7 @@ Instructions .. note:: In order to run ``beacon-python`` Web Server requirements are as specified below: - * Python 3.8+; + * Python 3.10+; * running DB `PostgreSQL Server `_ 9.6+ (recommended 13). .. _env-setup: diff --git a/readthedocs.yml b/readthedocs.yml index 1773a400..0413e75c 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -4,7 +4,7 @@ build: image: latest requirements_file: docs/docs.txt python: - version: 3.8 + version: 3.10 use_system_site_packages: true setup_py_install: true extra_requirements: diff --git a/setup.py b/setup.py index f66dfa47..2dea20f9 100644 --- a/setup.py +++ b/setup.py @@ -33,8 +33,7 @@ "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Scientific/Engineering :: Bio-Informatics", "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.10", ], install_requires=[ "asyncpg==0.27.0", diff --git a/tests/test.ini b/tests/test.ini index 8aa4ad7a..f2aeb067 100644 --- a/tests/test.ini +++ b/tests/test.ini @@ -116,3 +116,7 @@ audience= # If your service is not part of any network or AAI, but you still want to use tokens # produced by other AAI parties, set this value to False to skip the audience validation step verify_aud=False + +# Comma separated list of trusted JKUs +# JTW with an untrusted JKU will be denied access +trusted_jkus=http://test.csc.fi/jwk diff --git a/tests/test_app.py b/tests/test_app.py index 8a1f0d7d..575c51bc 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -5,7 +5,7 @@ import json from authlib.jose import jwt import os -from test.support import EnvironmentVarGuard +from test.support.os_helper import EnvironmentVarGuard from aiocache import caches diff --git a/tests/test_basic.py b/tests/test_basic.py index 575e9e9f..7ec112d3 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1,5 +1,6 @@ import unittest import aiohttp +import unittest.mock from beacon_api.utils.db_load import parse_arguments, init_beacon_db, main from beacon_api.conf.config import init_db_pool from beacon_api.api.query import access_resolution @@ -8,7 +9,7 @@ from beacon_api.permissions.ga4gh import check_ga4gh_token, decode_passport, get_ga4gh_permissions from .test_app import PARAMS, generate_token from testfixtures import TempDirectory -from test.support import EnvironmentVarGuard +from test.support.os_helper import EnvironmentVarGuard def mock_token(bona_fide, permissions, auth): @@ -70,6 +71,11 @@ async def load_datafile(self, vcf, datafile, datasetId, n=1000, min_ac=1): return ["datasetId", "variants"] +async def mock_get_ga4gh_controlled(input): + """Mock retrieve dataset permissions.""" + return input + + class TestBasicFunctions(unittest.IsolatedAsyncioTestCase): """Test supporting functions.""" @@ -482,5 +488,53 @@ async def test_get_ga4gh_permissions(self, m_userinfo, m_decode, m_controlled, m self.assertEqual(bona_fide_status, True) +class TestCaseCheckJku(unittest.IsolatedAsyncioTestCase): + """Test case.""" + + @unittest.mock.patch("beacon_api.permissions.ga4gh.get_ga4gh_bona_fide") + @unittest.mock.patch("beacon_api.permissions.ga4gh.get_ga4gh_controlled", side_effect=mock_get_ga4gh_controlled) + @unittest.mock.patch("beacon_api.permissions.ga4gh.decode_passport") + @unittest.mock.patch("beacon_api.permissions.ga4gh.retrieve_user_data") + async def test_jku_check(self, m_userinfo, m_decode, m_controller, m_bonafide): + """Test trusted and untrusted jku.""" + # Test: trusted jku + m_userinfo.return_value = [""] + header = {"jku": "http://test.csc.fi/jwk"} + payload = {"ga4gh_visa_v1": {"type": "ControlledAccessGrants"}} + m_decode.return_value = header, payload + m_bonafide.return_value = False + dataset_permissions, bona_fide_status = await get_ga4gh_permissions({}) + self.assertEqual(dataset_permissions, [("", header)]) + self.assertEqual(bona_fide_status, False) + # Test: untrusted jku + m_userinfo.return_value = [""] + header = {"jku": "untrusted_jku"} + payload = {"ga4gh_visa_v1": {"type": "ControlledAccessGrants"}} + m_decode.return_value = header, payload + m_bonafide.return_value = False + dataset_permissions, bona_fide_status = await get_ga4gh_permissions({}) + self.assertEqual(dataset_permissions, []) + self.assertEqual(bona_fide_status, False) + + @unittest.mock.patch("beacon_api.permissions.ga4gh.OAUTH2_CONFIG", return_value={"trusted_jkus": ["a"]}) + @unittest.mock.patch("beacon_api.permissions.ga4gh.get_ga4gh_bona_fide") + @unittest.mock.patch("beacon_api.permissions.ga4gh.get_ga4gh_controlled", side_effect=mock_get_ga4gh_controlled) + @unittest.mock.patch("beacon_api.permissions.ga4gh.decode_passport") + @unittest.mock.patch("beacon_api.permissions.ga4gh.retrieve_user_data") + async def test_jku_check_not_active(self, m_userinfo, m_decode, m_controller, m_bonafide, m_config): + """Test if jku check is skipped when trusted_jkus config var is not set.""" + # Todo apua miten pääset ronkkimaan conf.oauth2_config.trusted_jkus:[str] + # m_conf.return_value = [] + # OAUTH2_CONFIG.trusted_jkus = [] + m_userinfo.return_value = [""] + header = {"jku": "very_legit_jku_trust_me_bro"} + payload = {"ga4gh_visa_v1": {"type": "ControlledAccessGrants"}} + m_decode.return_value = header, payload + m_bonafide.return_value = False + dataset_permissions, bona_fide_status = await get_ga4gh_permissions({}) + self.assertEqual(dataset_permissions, [("", header)]) + self.assertEqual(bona_fide_status, False) + + if __name__ == "__main__": unittest.main() diff --git a/tox.ini b/tox.ini index 2abdedb1..8292f696 100644 --- a/tox.ini +++ b/tox.ini @@ -52,8 +52,8 @@ deps = .[test] -rrequirements.txt # Stop after first failure -commands = py.test -x --cov=beacon_api tests/ --cov-fail-under=80 +commands = py.test -x --cov=beacon_api tests/ --cov-fail-under=80 --cov-report html [gh-actions] python = - 3.8: flake8, unit_tests, docs, bandit, mypy + 3.10: flake8, unit_tests, docs, bandit, mypy