diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c7d2a8f1..0e2df0ad 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: |
@@ -26,16 +26,16 @@ 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
strategy:
fail-fast: false
matrix:
- python-version: [ '3.7', '3.8', '3.9']
- ckan-version: ["2.9", "2.10"]
- name: Python ${{ matrix.python-version }} extension test
+ python-version: ['3.9', '3.10']
+ ckan-version: ["2.10", "2.11"]
+ name: Python ${{ matrix.python-version }} CKAN ${{ matrix.ckan-version }} extension test
services:
postgresql:
@@ -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/README.md b/README.md
index 9aef2eae..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.
@@ -135,6 +139,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/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/plugin.py b/ckanext/saml2auth/plugin.py
index 8e0ed57c..7976222a 100644
--- a/ckanext/saml2auth/plugin.py
+++ b/ckanext/saml2auth/plugin.py
@@ -120,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)
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/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'])
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/ckanext/saml2auth/views/saml2auth.py b/ckanext/saml2auth/views/saml2auth.py
index 36a6c0f7..798dfadf 100644
--- a/ckanext/saml2auth/views/saml2auth.py
+++ b/ckanext/saml2auth/views/saml2auth.py
@@ -23,12 +23,12 @@
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
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
@@ -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):
@@ -227,6 +232,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()
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 cc2da1c6..f433b8b6 100644
--- a/setup.py
+++ b/setup.py
@@ -34,15 +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.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,
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''',
@@ -65,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',
],