From 6d14a8002a9c2705348e20ae53269b0a1abe1612 Mon Sep 17 00:00:00 2001 From: avdata99 Date: Tue, 23 Jul 2024 08:34:09 -0300 Subject: [PATCH 01/33] Allow skip logout --- README.md | 2 ++ ckanext/saml2auth/plugin.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9aef2eae..f4316f48 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,8 @@ Optional: # Saml logout request preferred binding settings variable # Default: urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST ckanext.saml2auth.logout_expected_binding = urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST + # If you don't want to logout from external source you can use + ckanext.saml2auth.logout_expected_binding = skip-external-logout # Default fallback endpoint to redirect to if no RelayState provided in the SAML Response # Default: user.me (ie /dashboard) diff --git a/ckanext/saml2auth/plugin.py b/ckanext/saml2auth/plugin.py index 8e0ed57c..bdb14680 100644 --- a/ckanext/saml2auth/plugin.py +++ b/ckanext/saml2auth/plugin.py @@ -97,7 +97,11 @@ def update_config(self, config_): def logout(self): - response = _perform_slo() + config = sp_config() + if config.get('logout_expected_binding') == 'skip-external-logout': + response = None + else: + response = _perform_slo() if response: domain = h.get_site_domain_for_cookie() From e738d36892f02d5c5caf36f12f882ee0ddcb99f5 Mon Sep 17 00:00:00 2001 From: avdata99 Date: Tue, 23 Jul 2024 08:46:10 -0300 Subject: [PATCH 02/33] Improve logout --- ckanext/saml2auth/plugin.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/ckanext/saml2auth/plugin.py b/ckanext/saml2auth/plugin.py index bdb14680..7976222a 100644 --- a/ckanext/saml2auth/plugin.py +++ b/ckanext/saml2auth/plugin.py @@ -97,11 +97,7 @@ def update_config(self, config_): def logout(self): - config = sp_config() - if config.get('logout_expected_binding') == 'skip-external-logout': - response = None - else: - response = _perform_slo() + response = _perform_slo() if response: domain = h.get_site_domain_for_cookie() @@ -124,9 +120,12 @@ def _perform_slo(): response = None - client = h.saml_client( - sp_config() - ) + config = sp_config() + if config.get('logout_expected_binding') == 'skip-external-logout': + log.debug('Skipping external logout') + return + + client = h.saml_client(config) saml_session_info = get_saml_session_info(session) subject_id = get_subject_id(session) From c613c9de67c944b939d6d07856f5ea63f0b78348 Mon Sep 17 00:00:00 2001 From: avdata99 Date: Fri, 26 Jul 2024 15:39:22 -0300 Subject: [PATCH 03/33] Trigger CKAN failed login signal --- ckanext/saml2auth/views/saml2auth.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ckanext/saml2auth/views/saml2auth.py b/ckanext/saml2auth/views/saml2auth.py index 36a6c0f7..70d4fefd 100644 --- a/ckanext/saml2auth/views/saml2auth.py +++ b/ckanext/saml2auth/views/saml2auth.py @@ -28,7 +28,7 @@ import ckan.model as model import ckan.plugins as plugins import ckan.lib.dictization.model_dictize as model_dictize -from ckan.lib import base +from ckan.lib import base, signals from ckan.views.user import set_repoze_user from ckan.common import config, g, request @@ -227,6 +227,8 @@ def acs(): if error is not None: log.error(error) extra_vars = {u'code': [400], u'content': error} + # Trigger the CKAN failed login signal + signals.failed_login.send('Unknown_SAML2_user') return base.render(u'error_document_template.html', extra_vars), 400 auth_response.get_identity() From 40520347a37713734943925063328eb1e054fa93 Mon Sep 17 00:00:00 2001 From: avdata99 Date: Wed, 31 Jul 2024 13:14:22 -0300 Subject: [PATCH 04/33] Fix flake8 --- .github/workflows/ci.yml | 2 +- ckanext/saml2auth/tests/test_helpers.py | 2 +- setup.py | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c7d2a8f1..02692e51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: - name: Lint with flake8 run: | - flake8 . --count --max-complexity=10 --max-line-length=127 --statistics --exclude ckan,ckanext-saml2auth + flake8 . --count --max-complexity=12 --max-line-length=127 --statistics --exclude ckan,ckanext-saml2auth test: runs-on: ubuntu-latest diff --git a/ckanext/saml2auth/tests/test_helpers.py b/ckanext/saml2auth/tests/test_helpers.py index 46d692e6..196cfe1d 100644 --- a/ckanext/saml2auth/tests/test_helpers.py +++ b/ckanext/saml2auth/tests/test_helpers.py @@ -30,7 +30,7 @@ def test_generate_password(): password = h.generate_password() assert len(password) == 8 - assert type(password) == str + assert isinstance(password, str) def test_default_login_disabled_by_default(): diff --git a/setup.py b/setup.py index cc2da1c6..e905d59f 100644 --- a/setup.py +++ b/setup.py @@ -41,8 +41,7 @@ long_description_content_type='text/markdown', # The project's main homepage. - url='https://github.com/keitaroinc/'\ - 'ckanext-saml2auth', + url='https://github.com/keitaroinc/ckanext-saml2auth', # Author details author='''Keitaro Inc''', From 32d67f8b815f8a25df54155d546bec62e205ee79 Mon Sep 17 00:00:00 2001 From: avdata99 Date: Wed, 31 Jul 2024 13:17:50 -0300 Subject: [PATCH 05/33] drop Py37 and CKAN 2.9 support --- .github/workflows/ci.yml | 53 ++----------------- bin/setup-ckan.bash | 5 +- .../saml2auth/tests/responses/unsigned0.xml | 2 +- .../tests/test_blueprint_get_request.py | 5 +- dev-requirements.txt | 1 + setup.py | 6 +-- 6 files changed, 17 insertions(+), 55 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02692e51..bdee55b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: '3.8' + python-version: '3.10' - name: Install flake8 run: | @@ -33,8 +33,8 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ '3.7', '3.8', '3.9'] - ckan-version: ["2.9", "2.10"] + python-version: ['3.10'] + ckan-version: ["2.10"] name: Python ${{ matrix.python-version }} extension test services: @@ -61,9 +61,7 @@ jobs: - 6379:6379 ckan-solr: - # Workflow level env variables are not addressable on job level, only on steps level - # image: ghcr.io/keitaroinc/ckan-solr-dev:{{ env.CKANVERSION }} - image: ghcr.io/keitaroinc/ckan-solr-dev:2.9 + image: ckan/ckan-solr:2.10 ports: - 8983:8983 @@ -90,46 +88,5 @@ jobs: - name: Test with pytest run: | + echo "Running SAML2AUTH tests" pytest --ckan-ini=subdir/test.ini --cov=ckanext.saml2auth --disable-warnings ckanext/saml2auth/tests - - - name: Coveralls - uses: AndreMiras/coveralls-python-action@develop - with: - parallel: true - flag-name: Python ${{ matrix.python-version }} Unit Test - - publish: - needs: test - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: '3.8' - - - name: Install setup requirements - run: | - python -m pip install --upgrade setuptools wheel twine - - - name: Build and package - run: | - python setup.py sdist bdist_wheel - twine check dist/* - - - name: Publish package - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@release/v1 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} - - coveralls_finish: - needs: test - runs-on: ubuntu-latest - steps: - - name: Coveralls Finished - uses: AndreMiras/coveralls-python-action@develop - with: - parallel-finished: true diff --git a/bin/setup-ckan.bash b/bin/setup-ckan.bash index 3ace4257..a1172a24 100755 --- a/bin/setup-ckan.bash +++ b/bin/setup-ckan.bash @@ -46,9 +46,10 @@ cd ckan ckan -c test-core.ini db init cd - -echo "Installing ckanext-saml2auth and its requirements..." -python setup.py develop +echo "Installing saml2 requirements..." pip install -r dev-requirements.txt +echo "Installing ckanext-saml2auth..." +pip install -e . echo "Moving test.ini into a subdir..." mkdir subdir diff --git a/ckanext/saml2auth/tests/responses/unsigned0.xml b/ckanext/saml2auth/tests/responses/unsigned0.xml index 7398230e..f2f74d54 100644 --- a/ckanext/saml2auth/tests/responses/unsigned0.xml +++ b/ckanext/saml2auth/tests/responses/unsigned0.xml @@ -17,7 +17,7 @@ _ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7 diff --git a/ckanext/saml2auth/tests/test_blueprint_get_request.py b/ckanext/saml2auth/tests/test_blueprint_get_request.py index 4095f54b..a844df66 100644 --- a/ckanext/saml2auth/tests/test_blueprint_get_request.py +++ b/ckanext/saml2auth/tests/test_blueprint_get_request.py @@ -58,7 +58,7 @@ def _prepare_unsigned_response(): 'entity_id': 'urn:gov:gsa:SAML:2.0.profiles:sp:sso:test:entity', 'destination': 'http://test.ckan.net/acs', 'recipient': 'http://test.ckan.net/acs', - 'issue_instant': datetime.now().isoformat() + 'issue_instant': datetime.now().isoformat(), } t = Template(unsigned_response) final_response = t.render(**context) @@ -118,6 +118,9 @@ def test_unsigned_request(self, app): 'SAMLResponse': encoded_response } response = app.post(url=url, params=data) + if response.status_code != 200: + assert False, f'Failed test_unsigned_request: {response.body}' + # Can't use response, too old (now=2024-07-31T17:42:38Z + slack=0 > not_on_or_after=2024-01-18T06:21:48Z assert 200 == response.status_code def render_file(self, path, context, save_as=None): diff --git a/dev-requirements.txt b/dev-requirements.txt index 61015aef..fcd5547a 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,2 +1,3 @@ flake8 # for the CI build pysaml2 +packaging>=22.0 diff --git a/setup.py b/setup.py index e905d59f..f433b8b6 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # http://packaging.python.org/en/latest/tutorial.html#version - version='1.3.0', + version='1.3.1', description='''An extension to enable Single Sign On(SSO) for CKAN data portals via SAML2 Authentication.''', long_description=long_description, @@ -64,9 +64,9 @@ # Specify the Python versions you support here. In particular, ensure # that you indicate whether you support Python 2, Python 3 or both. 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', ], From d40a753c115cd642116bb2add78f30e3eeeae9cc Mon Sep 17 00:00:00 2001 From: avdata99 Date: Wed, 31 Jul 2024 15:05:10 -0300 Subject: [PATCH 06/33] Increse coverage --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bdee55b3..56c61c79 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,8 +33,8 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.10'] - ckan-version: ["2.10"] + python-version: ['3.8', '3.9', '3.10'] + ckan-version: ["2.9", "2.10"] name: Python ${{ matrix.python-version }} extension test services: From 86d7cb7de85f15ad42ed0bf876c64212dafd40bb Mon Sep 17 00:00:00 2001 From: avdata99 Date: Wed, 31 Jul 2024 15:10:59 -0300 Subject: [PATCH 07/33] drop failing CKAN 2.9 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56c61c79..a63f371e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: fail-fast: false matrix: python-version: ['3.8', '3.9', '3.10'] - ckan-version: ["2.9", "2.10"] + ckan-version: ["2.10"] name: Python ${{ matrix.python-version }} extension test services: From f59f258978e5a68219ca389a7dd0d7b62fd931d8 Mon Sep 17 00:00:00 2001 From: avdata99 Date: Wed, 31 Jul 2024 15:12:18 -0300 Subject: [PATCH 08/33] README notes --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index f4316f48..bfbb573f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ [![CI][]][1] [![Coverage][]][2] [![Gitter][]][3] [![Pypi][]][4] [![Python][]][5] [![CKAN][]][6] +# Temporary fork + +**This is a temporary fork from OKFN** to work with CKAN 2.10 waiting for upstream repo at https://github.com/keitaroinc/ckanext-saml2auth to be updated. + # ckanext-saml2auth A [CKAN](https://ckan.org) extension to enable Single Sign-On (SSO) for CKAN data portals via SAML2 Authentication. From 0abb4ddc298d2aa86526d830ec131d3601978765 Mon Sep 17 00:00:00 2001 From: avdata99 Date: Wed, 31 Jul 2024 15:14:01 -0300 Subject: [PATCH 09/33] rename test --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a63f371e..c0bcde28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: matrix: python-version: ['3.8', '3.9', '3.10'] ckan-version: ["2.10"] - name: Python ${{ matrix.python-version }} extension test + name: Python ${{ matrix.python-version }} CKAN {{ matrix.ckan-version }} extension test services: postgresql: From 2774947f960acaf482b3644a11670df0b707ceca Mon Sep 17 00:00:00 2001 From: avdata99 Date: Wed, 31 Jul 2024 15:18:19 -0300 Subject: [PATCH 10/33] $ --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0bcde28..7c143305 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: matrix: python-version: ['3.8', '3.9', '3.10'] ckan-version: ["2.10"] - name: Python ${{ matrix.python-version }} CKAN {{ matrix.ckan-version }} extension test + name: Python ${{ matrix.python-version }} CKAN ${{ matrix.ckan-version }} extension test services: postgresql: From d8ee89b06a53592f3b1159a988a72167b92ee659 Mon Sep 17 00:00:00 2001 From: avdata99 Date: Wed, 23 Oct 2024 13:51:07 -0300 Subject: [PATCH 11/33] Search email ignoring case Do not allow to multiple users with same email --- ckanext/saml2auth/views/saml2auth.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/ckanext/saml2auth/views/saml2auth.py b/ckanext/saml2auth/views/saml2auth.py index 70d4fefd..798dfadf 100644 --- a/ckanext/saml2auth/views/saml2auth.py +++ b/ckanext/saml2auth/views/saml2auth.py @@ -23,7 +23,7 @@ from flask import Blueprint, session from saml2 import entity from saml2.authn_context import requested_authn_context - +from sqlalchemy.sql import func import ckan.plugins.toolkit as toolkit import ckan.model as model import ckan.plugins as plugins @@ -76,13 +76,18 @@ def _get_user_by_saml_id(saml_id): def _get_user_by_email(email): - user = model.User.by_email(email) - if user and isinstance(user, list): - user = user[0] + users = model.Session.query(model.User).filter( + func.lower(model.User.email) == func.lower(email) + ).all() - h.activate_user_if_deleted(user) + if len(users) == 0: + return None + if len(users) > 1: + raise toolkit.ValidationError(f'Multiple users with the same email found {email}') - return _dictize_user(user) if user else None + user = users[0] + h.activate_user_if_deleted(user) + return _dictize_user(user) def _update_user(user_dict): From 67fe0a8deadbf07bb6ab36362064ec05ef836ce7 Mon Sep 17 00:00:00 2001 From: Andres Vazquez Date: Mon, 28 Oct 2024 10:50:13 -0300 Subject: [PATCH 12/33] Add test for _get_user_by_email (#6) Add test for _get_user_by_email --- .../saml2auth/tests/test_get_user_by_email.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 ckanext/saml2auth/tests/test_get_user_by_email.py diff --git a/ckanext/saml2auth/tests/test_get_user_by_email.py b/ckanext/saml2auth/tests/test_get_user_by_email.py new file mode 100644 index 00000000..1a25e149 --- /dev/null +++ b/ckanext/saml2auth/tests/test_get_user_by_email.py @@ -0,0 +1,46 @@ +import pytest +from types import SimpleNamespace +import ckan.model as model +from ckan.tests import factories +from ckan.plugins import toolkit +from ckanext.saml2auth.views.saml2auth import _get_user_by_email + + +@pytest.fixture +def tdv_data(): + """TestDatasetViews setup data""" + obj = SimpleNamespace() + obj.user1 = factories.User( + email='user1@example.com', + plugin_extras={'saml2auth': {'saml_id': 'saml_id1'}} + ) + obj.user2 = factories.User( + email='user2@example.com', + plugin_extras={'saml2auth': {'saml_id': 'saml_id2'}} + ) + return obj + + +@pytest.mark.usefixtures(u'clean_db', u'clean_index') +@pytest.mark.ckan_config(u'ckan.plugins', u'saml2auth') +class TestDatasetViews(object): + def test_get_user_by_email_empty(self, tdv_data): + """ The the function _get_user_by_email for empty response """ + ret = _get_user_by_email('user3@example.com') + assert ret is None + + def test_get_user_by_email_ok(self, tdv_data): + """ The the function _get_user_by_email for empty response """ + ret = _get_user_by_email(tdv_data.user1['email']) + assert ret is not None + assert ret['email'] == tdv_data.user1['email'] + + def test_get_user_by_email_multiple(self, tdv_data): + """ The the function _get_user_by_email for duplicated emails """ + # Generate a duplciate email + user2 = model.User.get(tdv_data.user2['id']) + user2.email = tdv_data.user1['email'].upper() + model.Session.commit() + + with pytest.raises(toolkit.ValidationError): + _get_user_by_email(tdv_data.user1['email']) From f5e0cfddec613be9e6eb6202b56b4be3c76244b2 Mon Sep 17 00:00:00 2001 From: Patricio Del Boca Date: Wed, 15 Jan 2025 12:56:46 +0100 Subject: [PATCH 13/33] NameID no serializable --- ckanext/saml2auth/cache.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/ckanext/saml2auth/cache.py b/ckanext/saml2auth/cache.py index b169e6f4..20ac2a33 100644 --- a/ckanext/saml2auth/cache.py +++ b/ckanext/saml2auth/cache.py @@ -18,6 +18,7 @@ import logging from saml2.ident import code, decode +from saml2.saml import NameID log = logging.getLogger(__name__) @@ -34,11 +35,32 @@ def get_subject_id(session): def set_saml_session_info(session, saml_session_info): + """Adds information about pysaml2 AuthnResponse to CKAN's session. + + `pysaml2` returns a NameID object in the session_info() call. Since we want + to serialize the object to write it into the cookie we need to convert it. + `name_id` is the same as `_saml2_subject_id` so we apply `code` as we do in + `set_subject_id`. + + We are not sure if it always return an object, so we checking to be sure. + """ + if isinstance(saml_session_info['name_id'], NameID): + saml_session_info['name_id'] = code(saml_session_info['name_id']) session['_saml_session_info'] = saml_session_info def get_saml_session_info(session): + """Returns the saml session info from the session object. + + The session object is serializable but pysaml expect a NameID object as + name_id, so we are decoding it again as we do in get_subject_id. + """ try: - return session['_saml_session_info'] + session_info = session['_saml_session_info'] except KeyError: return None + + if isinstance(session_info['name_id'], str): + session_info['name_id'] = decode(session_info['name_id']) + + return session_info From 9bfa777a769d9adb3ddadc09bdf649d8bf5bba5b Mon Sep 17 00:00:00 2001 From: Andres Vazquez Date: Wed, 15 Jan 2025 09:44:24 -0300 Subject: [PATCH 14/33] Test CKAN 2.11 (#7) Update to CKAN 2.11 and release 1.3.3 --- .github/workflows/ci.yml | 4 ++-- ckanext/saml2auth/cache.py | 5 ++++- ckanext/saml2auth/tests/test_blueprint.py | 8 +++++++- .../saml2auth/tests/test_blueprint_get_request.py | 14 +++++++++++++- ckanext/saml2auth/tests/test_interface.py | 1 + conftest.py | 13 +++++++++---- setup.py | 13 ++++++++----- 7 files changed, 44 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c143305..b01a1a01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,8 +33,8 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.8', '3.9', '3.10'] - ckan-version: ["2.10"] + python-version: ['3.9', '3.10'] # TODO '3.11' + ckan-version: ["2.10", "2.11"] name: Python ${{ matrix.python-version }} CKAN ${{ matrix.ckan-version }} extension test services: diff --git a/ckanext/saml2auth/cache.py b/ckanext/saml2auth/cache.py index 20ac2a33..23357a81 100644 --- a/ckanext/saml2auth/cache.py +++ b/ckanext/saml2auth/cache.py @@ -24,7 +24,10 @@ def set_subject_id(session, subject_id): - session['_saml2_subject_id'] = code(subject_id) + if isinstance(subject_id, str): + session['_saml2_subject_id'] = subject_id + else: + session['_saml2_subject_id'] = code(subject_id) def get_subject_id(session): diff --git a/ckanext/saml2auth/tests/test_blueprint.py b/ckanext/saml2auth/tests/test_blueprint.py index 7e1a6eb5..cbb250e5 100644 --- a/ckanext/saml2auth/tests/test_blueprint.py +++ b/ckanext/saml2auth/tests/test_blueprint.py @@ -121,11 +121,17 @@ def test_ckan_cookie_cleared_on_slo(self, app): cookie_headers = [ h[1] for h in response.headers if h[0].lower() == 'set-cookie'] + # Sample for CKAN 2.11 + # ['ckan=; Domain=test.ckan.net; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/'] # Starting 2.10, CKAN's SessionMiddleware will append a # new Set-cookie header on every first response from the server. # This includes test requests. - assert len(cookie_headers) == 2 + # For CKAN 2.11, we only get the session cookie is named 'ckan' + if toolkit.check_ckan_version(min_version='2.11'): + assert len(cookie_headers) == 1 + else: + assert len(cookie_headers) == 2 first_cookie = cookie_headers[0] diff --git a/ckanext/saml2auth/tests/test_blueprint_get_request.py b/ckanext/saml2auth/tests/test_blueprint_get_request.py index a844df66..267dd957 100644 --- a/ckanext/saml2auth/tests/test_blueprint_get_request.py +++ b/ckanext/saml2auth/tests/test_blueprint_get_request.py @@ -117,10 +117,22 @@ def test_unsigned_request(self, app): data = { 'SAMLResponse': encoded_response } - response = app.post(url=url, params=data) + try: + response = app.post(url=url, params=data) + except Exception as e: + # decode the response + import base64 + decoded = base64.b64decode(encoded_response) + raise Exception( + f'Error test_unsigned_request: {e}\n' + f'encoded_response: {decoded}\n' + f'url: {url}\n' + ) + if response.status_code != 200: assert False, f'Failed test_unsigned_request: {response.body}' # Can't use response, too old (now=2024-07-31T17:42:38Z + slack=0 > not_on_or_after=2024-01-18T06:21:48Z + assert 200 == response.status_code def render_file(self, path, context, save_as=None): diff --git a/ckanext/saml2auth/tests/test_interface.py b/ckanext/saml2auth/tests/test_interface.py index 06ee4680..5940001b 100644 --- a/ckanext/saml2auth/tests/test_interface.py +++ b/ckanext/saml2auth/tests/test_interface.py @@ -40,6 +40,7 @@ class ExampleISaml2AuthPlugin(plugins.SingletonPlugin): def __init__(self, *args, **kwargs): self.calls = defaultdict(int) + super().__init__(*args, **kwargs) def before_saml2_user_update(self, user_dict, saml_attributes): diff --git a/conftest.py b/conftest.py index 4f5911d4..2607f6cc 100644 --- a/conftest.py +++ b/conftest.py @@ -17,7 +17,12 @@ along with this program. If not, see . """ -pytest_plugins = [ - u'ckan.tests.pytest_ckan.ckan_setup', - u'ckan.tests.pytest_ckan.fixtures', -] +from ckan.plugins import toolkit + + +if toolkit.check_ckan_version(max_version='2.10.99'): + + pytest_plugins = [ + u'ckan.tests.pytest_ckan.ckan_setup', + u'ckan.tests.pytest_ckan.fixtures', + ] diff --git a/setup.py b/setup.py index f433b8b6..622de4e9 100644 --- a/setup.py +++ b/setup.py @@ -34,17 +34,17 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # http://packaging.python.org/en/latest/tutorial.html#version - version='1.3.1', + version='1.3.3', description='''An extension to enable Single Sign On(SSO) for CKAN data portals via SAML2 Authentication.''', long_description=long_description, long_description_content_type='text/markdown', # The project's main homepage. - url='https://github.com/keitaroinc/ckanext-saml2auth', + url='https://github.com/okfn/ckanext-saml2auth', # Author details - author='''Keitaro Inc''', + author='''Keitaro Inc + OKFN''', author_email='''info@keitaro.com''', # Choose your license @@ -64,9 +64,9 @@ # Specify the Python versions you support here. In particular, ensure # that you indicate whether you support Python 2, Python 3 or both. 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', ], @@ -78,7 +78,10 @@ packages=find_packages(exclude=['contrib', 'docs', 'tests*']), namespace_packages=['ckanext'], - install_requires=['pysaml2>=6.5.1,<7.4'], + # pysaml2 7.4 requires python 3.9 + install_requires=[ + 'pysaml2>=7.4', + ], # If there are data files included in your packages that need to be # installed, specify them here. If using Python 2.6 or less, then these From f5c8429e9359163960dfbf1490e2145b0ba78fab Mon Sep 17 00:00:00 2001 From: Andres Vazquez Date: Thu, 13 Mar 2025 08:40:30 -0300 Subject: [PATCH 15/33] Improve error log (#10) * Improve error log * add e --- ckanext/saml2auth/views/saml2auth.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ckanext/saml2auth/views/saml2auth.py b/ckanext/saml2auth/views/saml2auth.py index 798dfadf..1f053c45 100644 --- a/ckanext/saml2auth/views/saml2auth.py +++ b/ckanext/saml2auth/views/saml2auth.py @@ -243,7 +243,14 @@ def acs(): # SAML username - unique saml_id = user_info.text # Required user attributes for user creation - email = auth_response.ava[saml_user_email][0] + try: + email = auth_response.ava[saml_user_email][0] + except KeyError as e: + error = 'User email not found in the SAML response' + error_internal = f'{error}: {e}. Data:{auth_response.ava}' + log.critical(error_internal) + extra_vars = {u'code': [400], u'content': error} + return base.render(u'error_document_template.html', extra_vars), 400 if saml_user_firstname and saml_user_lastname: first_name = auth_response.ava.get(saml_user_firstname, [email.split('@')[0]])[0] From 5f03659809da835ea747f0d9ee157d60517fd06b Mon Sep 17 00:00:00 2001 From: Favaro german <92406879+germankay@users.noreply.github.com> Date: Mon, 17 Mar 2025 09:37:24 -0300 Subject: [PATCH 16/33] Change the SSO button text (#11) Change the SSO button text --- ckanext/saml2auth/helpers.py | 9 +++++++++ ckanext/saml2auth/plugin.py | 4 ++-- .../saml2auth/templates/user/snippets/login_form.html | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/ckanext/saml2auth/helpers.py b/ckanext/saml2auth/helpers.py index 64d5f332..c46c5c06 100644 --- a/ckanext/saml2auth/helpers.py +++ b/ckanext/saml2auth/helpers.py @@ -121,3 +121,12 @@ def get_site_domain_for_cookie(): parsed_url = urlparse(site_url) host = parsed_url.netloc.split(':')[0] return host if '.' in host else None + + +def get_saml2auth_login_button_text(): + """ + Returns the configured text for the SAML2 login button. + Defaults to 'SSO' if not configured. + """ + text = toolkit.config.get('ckanext.saml2auth.login_button_text', 'SSO') + return text diff --git a/ckanext/saml2auth/plugin.py b/ckanext/saml2auth/plugin.py index 7976222a..50c06db2 100644 --- a/ckanext/saml2auth/plugin.py +++ b/ckanext/saml2auth/plugin.py @@ -48,8 +48,8 @@ class Saml2AuthPlugin(plugins.SingletonPlugin): def get_helpers(self): return { - 'is_default_login_enabled': - h.is_default_login_enabled + 'is_default_login_enabled': h.is_default_login_enabled, + 'get_saml2auth_login_button_text': h.get_saml2auth_login_button_text, } # IConfigurable diff --git a/ckanext/saml2auth/templates/user/snippets/login_form.html b/ckanext/saml2auth/templates/user/snippets/login_form.html index 43e89c3b..840639bb 100644 --- a/ckanext/saml2auth/templates/user/snippets/login_form.html +++ b/ckanext/saml2auth/templates/user/snippets/login_form.html @@ -18,5 +18,5 @@ {% ckan_extends %} {% block login_button %} - {{ _('SSO') }} + {{ h.get_saml2auth_login_button_text() }} {% endblock %} From 3c50674eae23c3c3f846116c8d0e276f5cbcfaf6 Mon Sep 17 00:00:00 2001 From: avdata99 Date: Mon, 17 Mar 2025 09:41:02 -0300 Subject: [PATCH 17/33] release 1.3.5 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 622de4e9..0ab08989 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # http://packaging.python.org/en/latest/tutorial.html#version - version='1.3.3', + version='1.3.5', description='''An extension to enable Single Sign On(SSO) for CKAN data portals via SAML2 Authentication.''', long_description=long_description, From 9b3a17f0e831a19e90d34190433700f2e7150692 Mon Sep 17 00:00:00 2001 From: blagoja Date: Tue, 29 Apr 2025 08:34:28 +0200 Subject: [PATCH 18/33] Add coverage tests --- .github/workflows/ci.yml | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b01a1a01..40166952 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,3 +90,45 @@ jobs: run: | echo "Running SAML2AUTH tests" pytest --ckan-ini=subdir/test.ini --cov=ckanext.saml2auth --disable-warnings ckanext/saml2auth/tests + + - name: Coveralls + uses: AndreMiras/coveralls-python-action@develop + with: + parallel: true + flag-name: Python ${{ matrix.python-version }} Unit Test + + publish: + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.8' + + - name: Install setup requirements + run: | + python -m pip install --upgrade setuptools wheel twine + + - name: Build and package + run: | + python setup.py sdist bdist_wheel + twine check dist/* + + - name: Publish package + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + + coveralls_finish: + needs: test + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: AndreMiras/coveralls-python-action@develop + with: + parallel-finished: true \ No newline at end of file From e73f351037a2c5901c25a7993ffbd3f3ebac4390 Mon Sep 17 00:00:00 2001 From: blagoja Date: Fri, 16 May 2025 18:49:29 +0200 Subject: [PATCH 19/33] Add tests for cache.py --- ckanext/saml2auth/tests/test_cache.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 ckanext/saml2auth/tests/test_cache.py diff --git a/ckanext/saml2auth/tests/test_cache.py b/ckanext/saml2auth/tests/test_cache.py new file mode 100644 index 00000000..8d0d791c --- /dev/null +++ b/ckanext/saml2auth/tests/test_cache.py @@ -0,0 +1,27 @@ +from ckanext.saml2auth.cache import set_subject_id +from types import SimpleNamespace +from saml2.ident import code + + +def test_set_subject_id_with_string(): + session = {} + set_subject_id(session, "user123") + assert session['_saml2_subject_id'] == "user123" + + +def test_set_subject_id_with_nameid_like_object(): + session = {} + + nameid = SimpleNamespace( + name_qualifier="issuer.example.com", + sp_name_qualifier="sp.example.com", + text="user123", + format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", + sp_provided_id=None + ) + + expected_code = code(nameid) + + set_subject_id(session, nameid) + + assert session['_saml2_subject_id'] == expected_code From 0f5ac7df8b3e3fe323162ff57621a485f74510c9 Mon Sep 17 00:00:00 2001 From: blagoja Date: Fri, 16 May 2025 19:04:03 +0200 Subject: [PATCH 20/33] Additional tests for cache.py --- ckanext/saml2auth/tests/test_cache.py | 32 ++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/ckanext/saml2auth/tests/test_cache.py b/ckanext/saml2auth/tests/test_cache.py index 8d0d791c..84c03a3b 100644 --- a/ckanext/saml2auth/tests/test_cache.py +++ b/ckanext/saml2auth/tests/test_cache.py @@ -1,4 +1,4 @@ -from ckanext.saml2auth.cache import set_subject_id +from ckanext.saml2auth.cache import set_subject_id, get_subject_id from types import SimpleNamespace from saml2.ident import code @@ -21,7 +21,33 @@ def test_set_subject_id_with_nameid_like_object(): ) expected_code = code(nameid) - set_subject_id(session, nameid) - assert session['_saml2_subject_id'] == expected_code + + +def test_get_subject_id_missing(): + session = {} + result = get_subject_id(session) + assert result is None + + +def test_get_subject_id_with_real_code_decode(): + + nameid = SimpleNamespace( + name_qualifier="issuer.example.com", + sp_name_qualifier="sp.example.com", + format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", + text="user123", + sp_provided_id=None # Include this to avoid AttributeError in `code()` + ) + + encoded = code(nameid) + session = {'_saml2_subject_id': encoded} + + result = get_subject_id(session) + + assert result.name_qualifier == nameid.name_qualifier + assert result.sp_name_qualifier == nameid.sp_name_qualifier + assert result.format == nameid.format + assert result.text == nameid.text + assert result.sp_provided_id == nameid.sp_provided_id From 6f0a36160fe658e88b0229beb753a0aae30f1715 Mon Sep 17 00:00:00 2001 From: blagoja Date: Fri, 16 May 2025 19:09:14 +0200 Subject: [PATCH 21/33] Tests code cleanup --- ckanext/saml2auth/tests/test_cache.py | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/ckanext/saml2auth/tests/test_cache.py b/ckanext/saml2auth/tests/test_cache.py index 84c03a3b..716a67ac 100644 --- a/ckanext/saml2auth/tests/test_cache.py +++ b/ckanext/saml2auth/tests/test_cache.py @@ -2,6 +2,14 @@ from types import SimpleNamespace from saml2.ident import code +nameid = SimpleNamespace( + name_qualifier="issuer.example.com", + sp_name_qualifier="sp.example.com", + text="user123", + format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", + sp_provided_id=None +) + def test_set_subject_id_with_string(): session = {} @@ -11,15 +19,6 @@ def test_set_subject_id_with_string(): def test_set_subject_id_with_nameid_like_object(): session = {} - - nameid = SimpleNamespace( - name_qualifier="issuer.example.com", - sp_name_qualifier="sp.example.com", - text="user123", - format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", - sp_provided_id=None - ) - expected_code = code(nameid) set_subject_id(session, nameid) assert session['_saml2_subject_id'] == expected_code @@ -32,14 +31,6 @@ def test_get_subject_id_missing(): def test_get_subject_id_with_real_code_decode(): - - nameid = SimpleNamespace( - name_qualifier="issuer.example.com", - sp_name_qualifier="sp.example.com", - format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", - text="user123", - sp_provided_id=None # Include this to avoid AttributeError in `code()` - ) encoded = code(nameid) session = {'_saml2_subject_id': encoded} From 384925c1a90ffbae23ac88719cdcdd8ee9d25529 Mon Sep 17 00:00:00 2001 From: blagoja Date: Fri, 16 May 2025 19:47:26 +0200 Subject: [PATCH 22/33] Tests code refactor --- ckanext/saml2auth/tests/test_cache.py | 44 ++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/ckanext/saml2auth/tests/test_cache.py b/ckanext/saml2auth/tests/test_cache.py index 716a67ac..748fdd5c 100644 --- a/ckanext/saml2auth/tests/test_cache.py +++ b/ckanext/saml2auth/tests/test_cache.py @@ -1,4 +1,4 @@ -from ckanext.saml2auth.cache import set_subject_id, get_subject_id +from ckanext.saml2auth.cache import set_subject_id, get_subject_id, get_saml_session_info from types import SimpleNamespace from saml2.ident import code @@ -42,3 +42,45 @@ def test_get_subject_id_with_real_code_decode(): assert result.format == nameid.format assert result.text == nameid.text assert result.sp_provided_id == nameid.sp_provided_id + + +def test_get_saml_session_info_with_encoded_nameid(): + + encoded_nameid = code(nameid) + session = { + '_saml_session_info': { + 'name_id': encoded_nameid, + 'other_data': 'example' + } + } + + result = get_saml_session_info(session) + + assert isinstance(result['name_id'], object) + assert result['name_id'].name_qualifier == nameid.name_qualifier + assert result['name_id'].sp_name_qualifier == nameid.sp_name_qualifier + assert result['name_id'].text == nameid.text + assert result['name_id'].format == nameid.format + assert result['name_id'].sp_provided_id == nameid.sp_provided_id + assert result['other_data'] == 'example' + + +def test_get_saml_session_info_with_decoded_nameid(): + + session = { + '_saml_session_info': { + 'name_id': nameid, + 'foo': 'bar' + } + } + + result = get_saml_session_info(session) + + assert result['name_id'] == nameid + assert result['foo'] == 'bar' + + +def test_get_saml_session_info_missing(): + session = {} + result = get_saml_session_info(session) + assert result is None From 25ad21555a50d76c1af4d72d04a944d84ffd3d4a Mon Sep 17 00:00:00 2001 From: blagoja Date: Sat, 17 May 2025 21:47:50 +0200 Subject: [PATCH 23/33] Disable conftests --- conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conftest.py b/conftest.py index 2607f6cc..89f42005 100644 --- a/conftest.py +++ b/conftest.py @@ -23,6 +23,6 @@ if toolkit.check_ckan_version(max_version='2.10.99'): pytest_plugins = [ - u'ckan.tests.pytest_ckan.ckan_setup', - u'ckan.tests.pytest_ckan.fixtures', + # u'ckan.tests.pytest_ckan.ckan_setup', + # u'ckan.tests.pytest_ckan.fixtures', ] From 4f5fc4c74bb9198608f01a7d6eb025a139804575 Mon Sep 17 00:00:00 2001 From: blagoja Date: Mon, 19 May 2025 15:08:55 +0200 Subject: [PATCH 24/33] Add slo unit tests --- ckanext/saml2auth/tests/test_saml2_logout.py | 29 ++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 ckanext/saml2auth/tests/test_saml2_logout.py diff --git a/ckanext/saml2auth/tests/test_saml2_logout.py b/ckanext/saml2auth/tests/test_saml2_logout.py new file mode 100644 index 00000000..fa158fdf --- /dev/null +++ b/ckanext/saml2auth/tests/test_saml2_logout.py @@ -0,0 +1,29 @@ +import pytest +from ckanext.saml2auth.plugin import _perform_slo +from unittest import mock +import os + +here = os.path.dirname(os.path.abspath(__file__)) +extras_folder = os.path.join(here, 'extras') +responses_folder = os.path.join(here, 'responses') + + +@pytest.mark.ckan_config(u'ckanext.saml2auth.logout_expected_binding', 'skip-external-logout') +def test_skip_external_logout(): + + response = _perform_slo() + + assert response is None + + +@pytest.mark.ckan_config(u'ckanext.saml2auth.idp_metadata.location', u'local') +@pytest.mark.ckan_config(u'ckanext.saml2auth.idp_metadata.local_path', os.path.join(extras_folder, 'provider0', 'idp.xml')) +def test_perform_slo_no_subject_id(): + # Mock session + with mock.patch('ckanext.saml2auth.plugin.session', {}), \ + mock.patch('ckanext.saml2auth.plugin.g', mock.Mock(user='test_user')), \ + mock.patch('ckanext.saml2auth.plugin.get_subject_id', return_value=None): + + response = _perform_slo() + + assert response is None \ No newline at end of file From 8bc614269228a4ecb753fb9cd488ee807bdea419 Mon Sep 17 00:00:00 2001 From: blagoja Date: Mon, 19 May 2025 15:12:50 +0200 Subject: [PATCH 25/33] Flake8 fixes --- ckanext/saml2auth/tests/test_saml2_logout.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ckanext/saml2auth/tests/test_saml2_logout.py b/ckanext/saml2auth/tests/test_saml2_logout.py index fa158fdf..5a3c1f22 100644 --- a/ckanext/saml2auth/tests/test_saml2_logout.py +++ b/ckanext/saml2auth/tests/test_saml2_logout.py @@ -23,7 +23,7 @@ def test_perform_slo_no_subject_id(): with mock.patch('ckanext.saml2auth.plugin.session', {}), \ mock.patch('ckanext.saml2auth.plugin.g', mock.Mock(user='test_user')), \ mock.patch('ckanext.saml2auth.plugin.get_subject_id', return_value=None): - + response = _perform_slo() - assert response is None \ No newline at end of file + assert response is None From 0bf0bca99c9d68a190a1f824e1900b243de5f451 Mon Sep 17 00:00:00 2001 From: blagoja Date: Mon, 19 May 2025 15:54:31 +0200 Subject: [PATCH 26/33] Remove unit test for CKAN 2.9 --- ckanext/saml2auth/tests/test_blueprint.py | 46 ----------------------- 1 file changed, 46 deletions(-) diff --git a/ckanext/saml2auth/tests/test_blueprint.py b/ckanext/saml2auth/tests/test_blueprint.py index cbb250e5..06237a0c 100644 --- a/ckanext/saml2auth/tests/test_blueprint.py +++ b/ckanext/saml2auth/tests/test_blueprint.py @@ -96,49 +96,3 @@ def test_cookies_cleared_on_slo(self, app): assert cookie[cookie_name]['domain'] == 'test.ckan.net' cookie_date = date_parse(cookie[cookie_name]['expires'], ignoretz=True) assert cookie_date < datetime.datetime.now() - - @pytest.mark.ckan_config(u'ckanext.saml2auth.idp_metadata.location', u'local') - @pytest.mark.ckan_config(u'ckanext.saml2auth.idp_metadata.local_path', - os.path.join(extras_folder, 'provider2', 'idp.xml')) - @pytest.mark.usefixtures('with_request_context') - def test_ckan_cookie_cleared_on_slo(self, app): - if not toolkit.check_ckan_version(min_version='2.10'): - # Remove when dropping support for 2.9 - pytest.skip("This test logic introduced in CKAN 2.10") - url = url_for('user.logout') - - import datetime - from unittest import mock - from http.cookies import SimpleCookie - from flask import make_response - from dateutil.parser import parse as date_parse - - with mock.patch( - 'ckanext.saml2auth.plugin._perform_slo', - return_value=make_response('')): - response = app.get(url=url, follow_redirects=False) - - cookie_headers = [ - h[1] for h in response.headers - if h[0].lower() == 'set-cookie'] - # Sample for CKAN 2.11 - # ['ckan=; Domain=test.ckan.net; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/'] - - # Starting 2.10, CKAN's SessionMiddleware will append a - # new Set-cookie header on every first response from the server. - # This includes test requests. - # For CKAN 2.11, we only get the session cookie is named 'ckan' - if toolkit.check_ckan_version(min_version='2.11'): - assert len(cookie_headers) == 1 - else: - assert len(cookie_headers) == 2 - - first_cookie = cookie_headers[0] - - cookie = SimpleCookie() - cookie.load(first_cookie) - cookie_name = [name for name in cookie.keys()][0] - assert cookie_name == 'ckan' - assert cookie[cookie_name]['domain'] == 'test.ckan.net' - cookie_date = date_parse(cookie[cookie_name]['expires'], ignoretz=True) - assert cookie_date < datetime.datetime.now() From 8b84a4e895579e8a7eba90254d0c20f74042898f Mon Sep 17 00:00:00 2001 From: blagoja Date: Mon, 19 May 2025 16:15:37 +0200 Subject: [PATCH 27/33] Revert change to tests --- ckanext/saml2auth/tests/test_blueprint.py | 46 +++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/ckanext/saml2auth/tests/test_blueprint.py b/ckanext/saml2auth/tests/test_blueprint.py index 06237a0c..3bed7665 100644 --- a/ckanext/saml2auth/tests/test_blueprint.py +++ b/ckanext/saml2auth/tests/test_blueprint.py @@ -96,3 +96,49 @@ def test_cookies_cleared_on_slo(self, app): assert cookie[cookie_name]['domain'] == 'test.ckan.net' cookie_date = date_parse(cookie[cookie_name]['expires'], ignoretz=True) assert cookie_date < datetime.datetime.now() + + @pytest.mark.ckan_config(u'ckanext.saml2auth.idp_metadata.location', u'local') + @pytest.mark.ckan_config(u'ckanext.saml2auth.idp_metadata.local_path', + os.path.join(extras_folder, 'provider2', 'idp.xml')) + @pytest.mark.usefixtures('with_request_context') + def test_ckan_cookie_cleared_on_slo(self, app): + if not toolkit.check_ckan_version(min_version='2.10'): + # Remove when dropping support for 2.9 + pytest.skip("This test logic introduced in CKAN 2.10") + url = url_for('user.logout') + + import datetime + from unittest import mock + from http.cookies import SimpleCookie + from flask import make_response + from dateutil.parser import parse as date_parse + + with mock.patch( + 'ckanext.saml2auth.plugin._perform_slo', + return_value=make_response('')): + response = app.get(url=url, follow_redirects=False) + + cookie_headers = [ + h[1] for h in response.headers + if h[0].lower() == 'set-cookie'] + # Sample for CKAN 2.11 + # ['ckan=; Domain=test.ckan.net; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/'] + + # Starting 2.10, CKAN's SessionMiddleware will append a + # new Set-cookie header on every first response from the server. + # This includes test requests. + # For CKAN 2.11, we only get the session cookie is named 'ckan' + if toolkit.check_ckan_version(min_version='2.11'): + assert len(cookie_headers) == 1 + else: + assert len(cookie_headers) == 2 + + first_cookie = cookie_headers[0] + + cookie = SimpleCookie() + cookie.load(first_cookie) + cookie_name = [name for name in cookie.keys()][0] + assert cookie_name == 'ckan' + assert cookie[cookie_name]['domain'] == 'test.ckan.net' + cookie_date = date_parse(cookie[cookie_name]['expires'], ignoretz=True) + assert cookie_date < datetime.datetime.now() \ No newline at end of file From fe79a001c7dc4bb2797b3433ee3e0924ca0cd84f Mon Sep 17 00:00:00 2001 From: blagoja Date: Mon, 19 May 2025 16:17:03 +0200 Subject: [PATCH 28/33] Flake8 fix --- ckanext/saml2auth/tests/test_blueprint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckanext/saml2auth/tests/test_blueprint.py b/ckanext/saml2auth/tests/test_blueprint.py index 3bed7665..cbb250e5 100644 --- a/ckanext/saml2auth/tests/test_blueprint.py +++ b/ckanext/saml2auth/tests/test_blueprint.py @@ -141,4 +141,4 @@ def test_ckan_cookie_cleared_on_slo(self, app): assert cookie_name == 'ckan' assert cookie[cookie_name]['domain'] == 'test.ckan.net' cookie_date = date_parse(cookie[cookie_name]['expires'], ignoretz=True) - assert cookie_date < datetime.datetime.now() \ No newline at end of file + assert cookie_date < datetime.datetime.now() From b0d2f8b4500e7028629903bea7d08cb5dd39a6ea Mon Sep 17 00:00:00 2001 From: blagoja Date: Mon, 19 May 2025 16:35:16 +0200 Subject: [PATCH 29/33] Drop CKAN 2.9 support for test ckan cookie --- ckanext/saml2auth/tests/test_blueprint.py | 40 +---------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/ckanext/saml2auth/tests/test_blueprint.py b/ckanext/saml2auth/tests/test_blueprint.py index cbb250e5..03f71a25 100644 --- a/ckanext/saml2auth/tests/test_blueprint.py +++ b/ckanext/saml2auth/tests/test_blueprint.py @@ -61,50 +61,12 @@ def test_came_from_sent_as_relay_state(self, app): response = app.get(url=url, follow_redirects=False) assert 'RelayState=%2Fdataset%2Fmy-dataset' in response.headers['Location'] - @pytest.mark.ckan_config(u'ckanext.saml2auth.idp_metadata.location', u'local') - @pytest.mark.ckan_config(u'ckanext.saml2auth.idp_metadata.local_path', - os.path.join(extras_folder, 'provider2', 'idp.xml')) - @pytest.mark.usefixtures('with_request_context') - def test_cookies_cleared_on_slo(self, app): - if toolkit.check_ckan_version(min_version='2.10'): - # Remove when dropping support for 2.9 - pytest.skip("auth_tkt cookie has been deprecated in 2.10") - url = url_for('user.logout') - - import datetime - from unittest import mock - from http.cookies import SimpleCookie - from flask import make_response - from dateutil.parser import parse as date_parse - - with mock.patch( - 'ckanext.saml2auth.plugin._perform_slo', - return_value=make_response('')): - response = app.get(url=url, follow_redirects=False) - - cookie_headers = [ - h[1] for h in response.headers - if h[0].lower() == 'set-cookie'] - - assert len(cookie_headers) == 2 - - for cookie_header in cookie_headers: - cookie = SimpleCookie() - cookie.load(cookie_header) - cookie_name = [name for name in cookie.keys()][0] - assert cookie_name in ['auth_tkt', 'ckan'] - assert cookie[cookie_name]['domain'] == 'test.ckan.net' - cookie_date = date_parse(cookie[cookie_name]['expires'], ignoretz=True) - assert cookie_date < datetime.datetime.now() - @pytest.mark.ckan_config(u'ckanext.saml2auth.idp_metadata.location', u'local') @pytest.mark.ckan_config(u'ckanext.saml2auth.idp_metadata.local_path', os.path.join(extras_folder, 'provider2', 'idp.xml')) @pytest.mark.usefixtures('with_request_context') def test_ckan_cookie_cleared_on_slo(self, app): - if not toolkit.check_ckan_version(min_version='2.10'): - # Remove when dropping support for 2.9 - pytest.skip("This test logic introduced in CKAN 2.10") + url = url_for('user.logout') import datetime From 838ffcd0ccac1e0a13900d092f3b4bb0528e6eb9 Mon Sep 17 00:00:00 2001 From: blagoja Date: Wed, 21 May 2025 10:21:57 +0200 Subject: [PATCH 30/33] Update README.md --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bfbb573f..2379a219 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,5 @@ [![CI][]][1] [![Coverage][]][2] [![Gitter][]][3] [![Pypi][]][4] [![Python][]][5] [![CKAN][]][6] -# Temporary fork - -**This is a temporary fork from OKFN** to work with CKAN 2.10 waiting for upstream repo at https://github.com/keitaroinc/ckanext-saml2auth to be updated. # ckanext-saml2auth @@ -10,7 +7,8 @@ A [CKAN](https://ckan.org) extension to enable Single Sign-On (SSO) for CKAN dat ## Requirements -This extension works with CKAN 2.9+. +This extension works with CKAN 2.10+ +Note: For CKAN 2.9 or older use v1.3.0 or older versions. ## Installation From c15822305fb746c5abec4608b13115535d25dcb3 Mon Sep 17 00:00:00 2001 From: blagoja Date: Wed, 21 May 2025 10:26:45 +0200 Subject: [PATCH 31/33] Update setup --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 0ab08989..24d55d4d 100644 --- a/setup.py +++ b/setup.py @@ -34,14 +34,14 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # http://packaging.python.org/en/latest/tutorial.html#version - version='1.3.5', + version='1.4.0', description='''An extension to enable Single Sign On(SSO) for CKAN data portals via SAML2 Authentication.''', long_description=long_description, long_description_content_type='text/markdown', # The project's main homepage. - url='https://github.com/okfn/ckanext-saml2auth', + url='https://github.com/keitaroinc/ckanext-saml2auth', # Author details author='''Keitaro Inc + OKFN''', From 0cbe81f6279d32e0d2623799cebb2b9a88f1d77d Mon Sep 17 00:00:00 2001 From: blagoja Date: Wed, 21 May 2025 10:35:42 +0200 Subject: [PATCH 32/33] Update setup --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 24d55d4d..a1225418 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ url='https://github.com/keitaroinc/ckanext-saml2auth', # Author details - author='''Keitaro Inc + OKFN''', + author='''Keitaro Inc''', author_email='''info@keitaro.com''', # Choose your license From 62cc2162f6fb381523c3c93c3d989f53973a755f Mon Sep 17 00:00:00 2001 From: blagoja Date: Wed, 21 May 2025 11:39:33 +0200 Subject: [PATCH 33/33] Remove additional plugins for conftest --- conftest.py | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 conftest.py diff --git a/conftest.py b/conftest.py deleted file mode 100644 index 89f42005..00000000 --- a/conftest.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Copyright (c) 2020 Keitaro AB - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -""" - -from ckan.plugins import toolkit - - -if toolkit.check_ckan_version(max_version='2.10.99'): - - pytest_plugins = [ - # u'ckan.tests.pytest_ckan.ckan_setup', - # u'ckan.tests.pytest_ckan.fixtures', - ]