Skip to content

Commit 269fc3c

Browse files
Merge branch 'keitaroinc:main' into feature/include_root_path_in_spconfig
2 parents 8fb0dcb + 8743a9f commit 269fc3c

20 files changed

+457
-105
lines changed

.github/workflows/ci.yml

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
- name: Setup Python
1818
uses: actions/setup-python@v4
1919
with:
20-
python-version: '3.8'
20+
python-version: '3.10'
2121

2222
- name: Install flake8
2323
run: |
@@ -26,16 +26,16 @@ jobs:
2626
2727
- name: Lint with flake8
2828
run: |
29-
flake8 . --count --max-complexity=10 --max-line-length=127 --statistics --exclude ckan,ckanext-saml2auth
29+
flake8 . --count --max-complexity=12 --max-line-length=127 --statistics --exclude ckan,ckanext-saml2auth
3030
3131
test:
3232
runs-on: ubuntu-latest
3333
strategy:
3434
fail-fast: false
3535
matrix:
36-
python-version: [ '3.7', '3.8', '3.9']
37-
ckan-version: ["2.9", "2.10"]
38-
name: Python ${{ matrix.python-version }} extension test
36+
python-version: ['3.9', '3.10'] # TODO '3.11'
37+
ckan-version: ["2.10", "2.11"]
38+
name: Python ${{ matrix.python-version }} CKAN ${{ matrix.ckan-version }} extension test
3939

4040
services:
4141
postgresql:
@@ -61,9 +61,7 @@ jobs:
6161
- 6379:6379
6262

6363
ckan-solr:
64-
# Workflow level env variables are not addressable on job level, only on steps level
65-
# image: ghcr.io/keitaroinc/ckan-solr-dev:{{ env.CKANVERSION }}
66-
image: ghcr.io/keitaroinc/ckan-solr-dev:2.9
64+
image: ckan/ckan-solr:2.10
6765
ports:
6866
- 8983:8983
6967

@@ -90,8 +88,9 @@ jobs:
9088
9189
- name: Test with pytest
9290
run: |
91+
echo "Running SAML2AUTH tests"
9392
pytest --ckan-ini=subdir/test.ini --cov=ckanext.saml2auth --disable-warnings ckanext/saml2auth/tests
94-
93+
9594
- name: Coveralls
9695
uses: AndreMiras/coveralls-python-action@develop
9796
with:
@@ -132,4 +131,4 @@ jobs:
132131
- name: Coveralls Finished
133132
uses: AndreMiras/coveralls-python-action@develop
134133
with:
135-
parallel-finished: true
134+
parallel-finished: true

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
[![CI][]][1] [![Coverage][]][2] [![Gitter][]][3] [![Pypi][]][4] [![Python][]][5] [![CKAN][]][6]
22

3+
34
# ckanext-saml2auth
45

56
A [CKAN](https://ckan.org) extension to enable Single Sign-On (SSO) for CKAN data portals via SAML2 Authentication.
67

78
## Requirements
89

9-
This extension works with CKAN 2.9+.
10+
This extension works with CKAN 2.10+
11+
Note: For CKAN 2.9 or older use v1.3.0 or older versions.
1012

1113
## Installation
1214

@@ -135,6 +137,8 @@ Optional:
135137
# Saml logout request preferred binding settings variable
136138
# Default: urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST
137139
ckanext.saml2auth.logout_expected_binding = urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST
140+
# If you don't want to logout from external source you can use
141+
ckanext.saml2auth.logout_expected_binding = skip-external-logout
138142

139143
# Default fallback endpoint to redirect to if no RelayState provided in the SAML Response
140144
# Default: user.me (ie /dashboard)
@@ -228,7 +232,7 @@ to PyPI follow these steps:
228232
[3]: https://gitter.im/keitaroinc/ckan?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge
229233
[Pypi]: https://img.shields.io/pypi/v/ckanext-saml2auth
230234
[4]: https://pypi.org/project/ckanext-saml2auth
231-
[Python]: https://img.shields.io/badge/python-3.7%20%7C%203.8%20%7C%203.9-blue
235+
[Python]: https://img.shields.io/badge/python-3.9%20|%203.10%20|%203.11-blue
232236
[5]: https://www.python.org
233-
[CKAN]: https://img.shields.io/badge/ckan-2.9%20%7C%202.10-yellow
237+
[CKAN]: https://img.shields.io/badge/ckan-2.10%20|%202.11-yellow
234238
[6]: https://www.ckan.org

bin/setup-ckan.bash

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@ cd ckan
4646
ckan -c test-core.ini db init
4747
cd -
4848

49-
echo "Installing ckanext-saml2auth and its requirements..."
50-
python setup.py develop
49+
echo "Installing saml2 requirements..."
5150
pip install -r dev-requirements.txt
51+
echo "Installing ckanext-saml2auth..."
52+
pip install -e .
5253

5354
echo "Moving test.ini into a subdir..."
5455
mkdir subdir

ckanext/saml2auth/cache.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,16 @@
1818
import logging
1919

2020
from saml2.ident import code, decode
21+
from saml2.saml import NameID
2122

2223
log = logging.getLogger(__name__)
2324

2425

2526
def set_subject_id(session, subject_id):
26-
session['_saml2_subject_id'] = code(subject_id)
27+
if isinstance(subject_id, str):
28+
session['_saml2_subject_id'] = subject_id
29+
else:
30+
session['_saml2_subject_id'] = code(subject_id)
2731

2832

2933
def get_subject_id(session):
@@ -34,11 +38,32 @@ def get_subject_id(session):
3438

3539

3640
def set_saml_session_info(session, saml_session_info):
41+
"""Adds information about pysaml2 AuthnResponse to CKAN's session.
42+
43+
`pysaml2` returns a NameID object in the session_info() call. Since we want
44+
to serialize the object to write it into the cookie we need to convert it.
45+
`name_id` is the same as `_saml2_subject_id` so we apply `code` as we do in
46+
`set_subject_id`.
47+
48+
We are not sure if it always return an object, so we checking to be sure.
49+
"""
50+
if isinstance(saml_session_info['name_id'], NameID):
51+
saml_session_info['name_id'] = code(saml_session_info['name_id'])
3752
session['_saml_session_info'] = saml_session_info
3853

3954

4055
def get_saml_session_info(session):
56+
"""Returns the saml session info from the session object.
57+
58+
The session object is serializable but pysaml expect a NameID object as
59+
name_id, so we are decoding it again as we do in get_subject_id.
60+
"""
4161
try:
42-
return session['_saml_session_info']
62+
session_info = session['_saml_session_info']
4363
except KeyError:
4464
return None
65+
66+
if isinstance(session_info['name_id'], str):
67+
session_info['name_id'] = decode(session_info['name_id'])
68+
69+
return session_info
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
version: 1
2+
groups:
3+
- annotation: ckanext-saml2auth settings
4+
options:
5+
- key: ckanext.saml2auth.idp_metadata.location
6+
example: local
7+
default: remote
8+
description: |
9+
Specifies the metadata location type.
10+
Options: local or remote
11+
required: true
12+
- key: ckanext.saml2auth.idp_metadata.local_path
13+
example: /opt/metadata/idp.xml
14+
default:
15+
description: |
16+
Path to a local file accessible on the server the service runs on.
17+
Ignore this config if the idp metadata location is set to: remote
18+
required: false
19+
- key: ckanext.saml2auth.idp_metadata.remote_url
20+
example: https://kalmar2.org/simplesaml/module.php/aggregator/?id=kalmarcentral2&set=saml2
21+
default:
22+
description: |
23+
A remote URL serving aggregate metadata
24+
Ignore this config if the idp metadata location is set to: local
25+
required: false
26+
- key: ckanext.saml2auth.idp_metadata.remote_cert
27+
example: /opt/metadata/kalmar2.cert
28+
default:
29+
description: |
30+
Path to a local file accessible on the server the service runs on
31+
Ignore this config if the idp metadata location is set to: local and metadata is public
32+
required: false
33+
- key: ckanext.saml2auth.user_firstname
34+
example: name
35+
default: firstname
36+
description: |
37+
Corresponding SAML user field for firstname
38+
required: false
39+
- key: ckanext.saml2auth.user_lastname
40+
example: surname
41+
default: lastname
42+
description: |
43+
Corresponding SAML user field for lastname
44+
required: false
45+
- key: ckanext.saml2auth.user_email
46+
example: emailadress
47+
default: email
48+
description: |
49+
Corresponding SAML user field for email
50+
required: true
51+
- key: ckanext.saml2auth.user_fullname
52+
example: fullname
53+
default: fullname
54+
description: |
55+
Corresponding SAML user field for fullname
56+
(Optional: Can be used as an alternative to firstname + lastname)
57+
required: false
58+
- key: ckanext.saml2auth.enable_ckan_internal_login
59+
example: True
60+
default: False
61+
description: |
62+
Configuration setting that enables CKAN's internal register/login functionality as well
63+
required: false
64+
type: bool
65+
- key: ckanext.saml2auth.sysadmins_list
66+
example: mail@domain.com mail2@domain.com mail3@domain.com
67+
default:
68+
description: |
69+
List of email addresses from users that should be created as sysadmins (system administrators)
70+
Note that this means that CKAN sysadmins will _only_ be managed based on this config option and will override existing user permissions in the CKAN database
71+
If not set then it is ignored and CKAN sysadmins are managed through normal means
72+
required: false
73+
- key: ckanext.saml2auth.allow_unknown_attributes
74+
example: False
75+
default: True
76+
description: |
77+
Indicates that attributes that are not recognized (they are not configured in attribute-mapping),
78+
will not be discarded.
79+
required: false
80+
type: bool
81+
- key: ckanext.saml2auth.sp.name_id_format
82+
example: urn:oasis:names:tc:SAML:2.0:nameid-format:transient
83+
default: urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
84+
description: |
85+
A list of string values that will be used to set the <NameIDFormat> element of the metadata of an entity.
86+
required: false
87+
- key: ckanext.saml2auth.sp.name_id_policy_format
88+
example: urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
89+
default:
90+
description: |
91+
A string value that will be used to set the Format attribute of the <NameIDPolicy> element of the metadata of an entity.
92+
required: false
93+
- key: ckanext.saml2auth.entity_id
94+
example: urn:gov:gsa:SAML:2.0.profiles:sp:sso:gsa:catalog-dev
95+
default: urn:mace:umu.se:saml:ckan:sp
96+
description: |
97+
Entity ID (also know as Issuer)
98+
Define the entity ID.
99+
required: false
100+
- key: ckanext.saml2auth.want_response_signed
101+
example: False
102+
default: True
103+
description: |
104+
Indicates if SAML responses must be signed.
105+
required: false
106+
type: bool
107+
- key: ckanext.saml2auth.want_assertions_signed
108+
example: True
109+
default: False
110+
description: |
111+
Indicates if SAML assertions must be signed.
112+
required: false
113+
type: bool
114+
- key: ckanext.saml2auth.want_assertions_or_response_signed
115+
example: True
116+
default: False
117+
description: |
118+
If set to true, either the SAML response or the assertion must be signed.
119+
required: false
120+
type: bool
121+
- key: ckanext.saml2auth.key_file_path
122+
example: /path/to/mykey.pem
123+
default:
124+
description: |
125+
Path to the private key file used to sign SAML requests or decrypt assertions.
126+
required: false
127+
- key: ckanext.saml2auth.cert_file_path
128+
example: /path/to/mycert.pem
129+
default:
130+
description: |
131+
Path to the public certificate file used to verify signatures or encrypt assertions.
132+
required: false
133+
- key: ckanext.saml2auth.attribute_map_dir
134+
example: /path/to/dir/attributemaps
135+
default:
136+
description: |
137+
Directory path containing SAML attribute mapping configuration files.
138+
required: false
139+
- key: ckanext.saml2auth.requested_authn_context
140+
example: http://idmanagement.gov/ns/assurance/aal/3?hspd12=true
141+
default:
142+
description: |
143+
Specifies authentication context class references requested during login.
144+
You can provide multiple values separated by spaces.
145+
required: false
146+
- key: ckanext.saml2auth.requested_authn_context_comparison
147+
example: exact
148+
default: exact
149+
description: |
150+
Comparison method for RequestedAuthnContext.
151+
Options: exact, minimum, maximum, better
152+
required: false
153+
- key: ckanext.saml2auth.logout_requests_signed
154+
example: True
155+
default: False
156+
description: |
157+
Indicates whether this service provider will sign logout requests.
158+
required: false
159+
type: bool
160+
- key: ckanext.saml2auth.logout_expected_binding
161+
example: urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST
162+
default: urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST
163+
description: |
164+
The expected SAML binding method for logout requests.
165+
required: false
166+
- key: ckanext.saml2auth.default_fallback_endpoint
167+
example: home.index
168+
default: user.me
169+
description: |
170+
The default endpoint to redirect to if no RelayState is provided in the SAML response.
171+
required: false

ckanext/saml2auth/helpers.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,12 @@ def get_site_domain_for_cookie():
121121
parsed_url = urlparse(site_url)
122122
host = parsed_url.netloc.split(':')[0]
123123
return host if '.' in host else None
124+
125+
126+
def get_saml2auth_login_button_text():
127+
"""
128+
Returns the configured text for the SAML2 login button.
129+
Defaults to 'SSO' if not configured.
130+
"""
131+
text = toolkit.config.get('ckanext.saml2auth.login_button_text', 'SSO')
132+
return text

ckanext/saml2auth/plugin.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
log = logging.getLogger(__name__)
3838

3939

40+
@toolkit.blanket.config_declarations
4041
class Saml2AuthPlugin(plugins.SingletonPlugin):
4142
plugins.implements(plugins.IConfigurer)
4243
plugins.implements(plugins.IBlueprint)
@@ -48,8 +49,8 @@ class Saml2AuthPlugin(plugins.SingletonPlugin):
4849

4950
def get_helpers(self):
5051
return {
51-
'is_default_login_enabled':
52-
h.is_default_login_enabled
52+
'is_default_login_enabled': h.is_default_login_enabled,
53+
'get_saml2auth_login_button_text': h.get_saml2auth_login_button_text,
5354
}
5455

5556
# IConfigurable
@@ -120,9 +121,12 @@ def _perform_slo():
120121

121122
response = None
122123

123-
client = h.saml_client(
124-
sp_config()
125-
)
124+
config = sp_config()
125+
if config.get('logout_expected_binding') == 'skip-external-logout':
126+
log.debug('Skipping external logout')
127+
return
128+
129+
client = h.saml_client(config)
126130
saml_session_info = get_saml_session_info(session)
127131
subject_id = get_subject_id(session)
128132

ckanext/saml2auth/templates/user/snippets/login_form.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@
1818
{% ckan_extends %}
1919
{% block login_button %}
2020
<button class="btn btn-primary" type="submit">{{ _('Login') }}</button>
21-
<a class="btn btn-default" href="{{ h.url_for('saml2auth.saml2login') }}">{{ _('SSO') }}</a>
21+
<a class="btn btn-default" href="{{ h.url_for('saml2auth.saml2login') }}">{{ h.get_saml2auth_login_button_text() }}</a>
2222
{% endblock %}

ckanext/saml2auth/tests/responses/unsigned0.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
<saml:NameID SPNameQualifier="{{ entity_id }}" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
1818
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
1919
<saml:SubjectConfirmationData
20-
NotOnOrAfter="2024-01-18T06:21:48Z"
20+
NotOnOrAfter="2026-01-18T06:21:48Z"
2121
Recipient="{{ recipient }}"
2222
InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"/>
2323
</saml:SubjectConfirmation>

0 commit comments

Comments
 (0)