Skip to content

Commit

Permalink
Merge pull request #37 from keitaroinc/support-fullname-field-ckan-2.8
Browse files Browse the repository at this point in the history
Support fullname field - backport to CKAN 2.8
  • Loading branch information
duskobogdanovski committed Feb 17, 2021
2 parents fc2f344 + 4600380 commit 0b4ffc8
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 10 deletions.
4 changes: 4 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ Required::
# Corresponding SAML user field for lastname
ckanext.saml2auth.user_lastname = lastname

# Corresponding SAML user field for fullname
# (Optional: Can be used as an alternative to firstname + lastname)
ckanext.saml2auth.user_fullname = fullname

# Corresponding SAML user field for email
ckanext.saml2auth.user_email = email

Expand Down
15 changes: 12 additions & 3 deletions ckanext/saml2auth/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ def configure(self, config):
# exception if they're missing.
missing_config = "{0} is not configured. Please amend your .ini file."
config_options = (
'ckanext.saml2auth.user_firstname',
'ckanext.saml2auth.user_lastname',
'ckanext.saml2auth.user_email'
'ckanext.saml2auth.user_email',
)
if not config.get('ckanext.saml2auth.idp_metadata.local_path'):
config_options += ('ckanext.saml2auth.idp_metadata.remote_url',
Expand All @@ -38,6 +36,17 @@ def configure(self, config):
if not config.get(option, None):
raise RuntimeError(missing_config.format(option))

first_and_last_name = all((
config.get('ckanext.saml2auth.user_firstname'),
config.get('ckanext.saml2auth.user_lastname')
))
full_name = config.get('ckanext.saml2auth.user_fullname')

if not first_and_last_name and not full_name:
raise RuntimeError('''
You need to provide both ckanext.saml2auth.user_firstname +
ckanext.saml2auth.user_lastname or ckanext.saml2auth.user_fullname'''.strip())

acs_endpoint = config.get('ckanext.saml2auth.acs_endpoint')
if acs_endpoint and not acs_endpoint.startswith('/'):
raise RuntimeError('ckanext.saml2auth.acs_endpoint should start with a slash ("/")')
Expand Down
9 changes: 9 additions & 0 deletions ckanext/saml2auth/tests/responses/unsigned0.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@
<saml:Attribute Name="email" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">test@example.com</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="firstname" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue>John</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="lastname" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue>Smith</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="fullname" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue>John Smith (Operations)</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="eduPersonAffiliation" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">users</saml:AttributeValue>
<saml:AttributeValue xsi:type="xs:string">examplerole1</saml:AttributeValue>
Expand Down
78 changes: 78 additions & 0 deletions ckanext/saml2auth/tests/test_blueprint_get_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import os
from saml import schema
from ckan.tests.helpers import FunctionalTestBase, change_config
from ckan import model

from saml2.xmldsig import SIG_RSA_SHA256
from saml2.xmldsig import DIGEST_SHA256
Expand Down Expand Up @@ -439,3 +440,80 @@ def test_signed_not_encrypted_assertion(self):
response = app.post(url=url, params=data)
# we expect a redirection after login
assert_equal(302, response.status_int)

@change_config(u'ckanext.saml2auth.entity_id', u'urn:gov:gsa:SAML:2.0.profiles:sp:sso:test:entity')
@change_config(u'ckanext.saml2auth.idp_metadata.location', u'local')
@change_config(u'ckanext.saml2auth.idp_metadata.local_path', os.path.join(extras_folder, 'provider0', 'idp.xml'))
@change_config(u'ckanext.saml2auth.want_response_signed', u'False')
@change_config(u'ckanext.saml2auth.want_assertions_signed', u'False')
@change_config(u'ckanext.saml2auth.want_assertions_or_response_signed', u'False')
def test_user_fullname_using_first_last_name(self):

# read about saml2 responses: https://www.samltool.com/generic_sso_res.php
unsigned_response_file = os.path.join(responses_folder, 'unsigned0.xml')
unsigned_response = open(unsigned_response_file).read()
# parse values
context = {
'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()
}
t = Template(unsigned_response)
final_response = t.render(**context)

encoded_response = base64.b64encode(final_response)

app = self._get_test_app()
url = '/acs'

data = {
'SAMLResponse': encoded_response
}
response = app.post(url=url, params=data)
# we expect a redirection after login
assert_equal(302, response.status_int)

user = model.User.by_email('test@example.com')[0]

assert_equal(user.fullname, 'John Smith')

@change_config(u'ckanext.saml2auth.entity_id', u'urn:gov:gsa:SAML:2.0.profiles:sp:sso:test:entity')
@change_config(u'ckanext.saml2auth.idp_metadata.location', u'local')
@change_config(u'ckanext.saml2auth.idp_metadata.local_path', os.path.join(extras_folder, 'provider0', 'idp.xml'))
@change_config(u'ckanext.saml2auth.want_response_signed', u'False')
@change_config(u'ckanext.saml2auth.want_assertions_signed', u'False')
@change_config(u'ckanext.saml2auth.want_assertions_or_response_signed', u'False')
@change_config(u'ckanext.saml2auth.user_fullname', u'fullname')
@change_config(u'ckanext.saml2auth.user_firstname', None)
@change_config(u'ckanext.saml2auth.user_lastname', None)
def test_user_fullname_using_fullname(self):

# read about saml2 responses: https://www.samltool.com/generic_sso_res.php
unsigned_response_file = os.path.join(responses_folder, 'unsigned0.xml')
unsigned_response = open(unsigned_response_file).read()
# parse values
context = {
'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()
}
t = Template(unsigned_response)
final_response = t.render(**context)

encoded_response = base64.b64encode(final_response)

app = self._get_test_app()
url = '/acs'

data = {
'SAMLResponse': encoded_response
}
response = app.post(url=url, params=data)
# we expect a redirection after login
assert_equal(302, response.status_int)

user = model.User.by_email('test@example.com')[0]

assert_equal(user.fullname, 'John Smith (Operations)')
23 changes: 17 additions & 6 deletions ckanext/saml2auth/views/saml2auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,25 @@ def get_ckan_user(email):
log.debug('CKAN user not found for {}'.format(email))


def create_user(context, email, firstname, lastname):
def create_user(context, email, full_name):
""" Create a new CKAN user from saml """
data_dict = {
u'name': h.ensure_unique_username_from_email(email),
u'fullname': u'{0} {1}'.format(firstname, lastname),
u'fullname': full_name,
u'email': email,
u'password': h.generate_password()
}

try:
return logic.get_action(u'user_create')(context, data_dict)
user_dict = logic.get_action(u'user_create')(context, data_dict)
log.info('CKAN user created: {}'.format(data_dict['name']))
except logic.ValidationError as e:
error_message = (e.error_summary or e.message or e.error_dict)
log.error(error_message)
base.abort(400, error_message)

return user_dict


def acs():
u'''The location where the SAML assertion is sent with a HTTP POST.
Expand All @@ -68,6 +70,8 @@ def acs():
config.get(u'ckanext.saml2auth.user_firstname')
saml_user_lastname = \
config.get(u'ckanext.saml2auth.user_lastname')
saml_user_fullname = \
config.get(u'ckanext.saml2auth.user_fullname')
saml_user_email = \
config.get(u'ckanext.saml2auth.user_email')

Expand Down Expand Up @@ -100,14 +104,21 @@ def acs():
# Required user attributes for user creation
email = auth_response.ava[saml_user_email][0]

firstname = auth_response.ava.get(saml_user_firstname, [email.split('@')[0]])[0]
lastname = auth_response.ava.get(saml_user_lastname, [email.split('@')[1]])[0]
if saml_user_firstname and saml_user_lastname:
first_name = auth_response.ava.get(saml_user_firstname, [email.split('@')[0]])[0]
last_name = auth_response.ava.get(saml_user_lastname, [email.split('@')[1]])[0]
full_name = u'{} {}'.format(first_name, last_name)
else:
if saml_user_fullname in auth_response.ava:
full_name = auth_response.ava[saml_user_fullname][0]
else:
full_name = u'{} {}'.format(email.split('@')[0], email.split('@')[1])

# Check if CKAN-SAML user exists for the current SAML login
user = get_ckan_user(email)

if not user:
user_dict = create_user(context, email, firstname, lastname)
user_dict = create_user(context, email, full_name)
else:
# If account exists and is deleted, reactivate it.
h.activate_user_if_deleted(user)
Expand Down
2 changes: 1 addition & 1 deletion test.ini
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ ckan.plugins = saml2auth

ckanext.saml2auth.idp_metadata.location = local
ckanext.saml2auth.idp_metadata.local_path = /path/to/idp.xml
ckanext.saml2auth.user_firstname = name
ckanext.saml2auth.user_firstname = firstname
ckanext.saml2auth.user_lastname = lastname
ckanext.saml2auth.user_email = email

Expand Down

0 comments on commit 0b4ffc8

Please sign in to comment.