Skip to content

Commit b71f144

Browse files
authored
Merge branch 'master' into master
2 parents 546f9d4 + 9e6ddc0 commit b71f144

38 files changed

+830
-180
lines changed

README.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,15 @@ testing. To run the tests on your system's version of python
3838

3939
To run tests in multiple python environments, you can use
4040
`pyenv <https://github.com/yyuu/pyenv>`_ with `tox <https://tox.readthedocs.io/en/latest/>`_.
41+
42+
43+
Please contribute!
44+
==================
45+
46+
To help out, you could:
47+
48+
1. Test and report any bugs or other difficulties.
49+
2. Implement missing features.
50+
3. Write more unit tests.
51+
52+
**If you have the time and inclination I'm looking for Collaborators**

src/saml2/algsupport.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"rsa-sha256": 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
2424
"rsa-sha384": 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384',
2525
"rsa-sha512": 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512',
26-
"dsa-sha1": 'http,//www.w3.org/2000/09/xmldsig#dsa-sha1',
26+
"dsa-sha1": 'http://www.w3.org/2000/09/xmldsig#dsa-sha1',
2727
'dsa-sha256': 'http://www.w3.org/2009/xmldsig11#dsa-sha256',
2828
'ecdsa_sha1': 'http://www.w3.org/2001/04/xmldsig-more#ECDSA_sha1',
2929
'ecdsa_sha224': 'http://www.w3.org/2001/04/xmldsig-more#ECDSA_sha224',

src/saml2/assertion.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,19 +78,22 @@ def filter_on_attributes(ava, required=None, optional=None, acs=None,
7878
"""
7979

8080
def _match_attr_name(attr, ava):
81-
try:
82-
friendly_name = attr["friendly_name"]
83-
except KeyError:
84-
friendly_name = get_local_name(acs, attr["name"],
85-
attr["name_format"])
81+
82+
local_name = get_local_name(acs, attr["name"], attr["name_format"])
83+
if not local_name:
84+
try:
85+
local_name = attr["friendly_name"]
86+
except KeyError:
87+
pass
8688

87-
_fn = _match(friendly_name, ava)
89+
_fn = _match(local_name, ava)
8890
if not _fn: # In the unlikely case that someone has provided us with
8991
# URIs as attribute names
9092
_fn = _match(attr["name"], ava)
9193

9294
return _fn
9395

96+
9497
def _apply_attr_value_restrictions(attr, res, must=False):
9598
try:
9699
values = [av["text"] for av in attr["attribute_value"]]
@@ -105,7 +108,6 @@ def _apply_attr_value_restrictions(attr, res, must=False):
105108
return _filter_values(ava[_fn], values, must)
106109

107110
res = {}
108-
109111
if required is None:
110112
required = []
111113

src/saml2/attribute_converter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ def get_local_name(acs, attr, name_format):
246246
for aconv in acs:
247247
#print(ac.format, name_format)
248248
if aconv.name_format == name_format:
249-
return aconv._fro[attr]
249+
return aconv._fro.get(attr)
250250

251251

252252
def d_to_local_name(acs, attr):

src/saml2/attributemaps/saml_uri.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,19 @@
1313
SIS = 'urn:oid:1.2.752.194.10.2.'
1414
UMICH = 'urn:oid:1.3.6.1.4.1.250.1.57.'
1515
OPENOSI_OID = 'urn:oid:1.3.6.1.4.1.27630.2.1.1.' #openosi-0.82.schema http://www.openosi.org/osi/display/ldap/Home
16+
EIDAS_NATURALPERSON = 'http://eidas.europa.eu/attributes/naturalperson/'
1617

1718
MAP = {
1819
'identifier': 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri',
1920
'fro': {
21+
EIDAS_NATURALPERSON+'PersonIdentifier': 'PersonIdentifier',
22+
EIDAS_NATURALPERSON+'FamilyName': 'FamilyName',
23+
EIDAS_NATURALPERSON+'FirstName': 'FirstName',
24+
EIDAS_NATURALPERSON+'DateOfBirth': 'DateOfBirth',
25+
EIDAS_NATURALPERSON+'BirthName': 'BirthName',
26+
EIDAS_NATURALPERSON+'PlaceOfBirth': 'PlaceOfBirth',
27+
EIDAS_NATURALPERSON+'CurrentAddress': 'CurrentAddress',
28+
EIDAS_NATURALPERSON+'Gender': 'Gender',
2029
EDUCOURSE_OID+'1': 'eduCourseOffering',
2130
EDUCOURSE_OID+'2': 'eduCourseMember',
2231
EDUMEMBER1_OID+'1': 'isMemberOf',
@@ -161,6 +170,14 @@
161170
X500ATTR_OID+'65': 'pseudonym',
162171
},
163172
'to': {
173+
'PersonIdentifier': EIDAS_NATURALPERSON+'PersonIdentifier',
174+
'FamilyName': EIDAS_NATURALPERSON+'FamilyName',
175+
'FirstName': EIDAS_NATURALPERSON+'FirstName',
176+
'DateOfBirth': EIDAS_NATURALPERSON+'DateOfBirth',
177+
'BirthName': EIDAS_NATURALPERSON+'BirthName',
178+
'PlaceOfBirth': EIDAS_NATURALPERSON+'PlaceOfBirth',
179+
'CurrentAddress': EIDAS_NATURALPERSON+'CurrentAddress',
180+
'Gender': EIDAS_NATURALPERSON+'Gender',
164181
'associatedDomain': UCL_DIR_PILOT+'37',
165182
'authorityRevocationList': X500ATTR_OID+'38',
166183
'businessCategory': X500ATTR_OID+'15',

src/saml2/client_base.py

Lines changed: 114 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
from saml2.entity import Entity
1212

13+
import saml2.attributemaps as attributemaps
14+
1315
from saml2.mdstore import destinations
1416
from saml2.profile import paos, ecp
1517
from saml2.saml import NAMEID_FORMAT_TRANSIENT
@@ -18,6 +20,9 @@
1820
from saml2.samlp import AttributeQuery
1921
from saml2.samlp import AuthzDecisionQuery
2022
from saml2.samlp import AuthnRequest
23+
from saml2.samlp import Extensions
24+
from saml2.extension import sp_type
25+
from saml2.extension import requested_attributes
2126

2227
import saml2
2328
import time
@@ -207,7 +212,7 @@ def create_authn_request(self, destination, vorg="", scoping=None,
207212
nameid_format=None,
208213
service_url_binding=None, message_id=0,
209214
consent=None, extensions=None, sign=None,
210-
allow_create=False, sign_prepare=False, sign_alg=None,
215+
allow_create=None, sign_prepare=False, sign_alg=None,
211216
digest_alg=None, **kwargs):
212217
""" Creates an authentication request.
213218
@@ -235,26 +240,30 @@ def create_authn_request(self, destination, vorg="", scoping=None,
235240

236241
args = {}
237242

238-
try:
239-
args["assertion_consumer_service_url"] = kwargs[
240-
"assertion_consumer_service_urls"][0]
241-
del kwargs["assertion_consumer_service_urls"]
242-
except KeyError:
243+
if self.config.getattr('hide_assertion_consumer_service', 'sp'):
244+
args["assertion_consumer_service_url"] = None
245+
binding = None
246+
else:
243247
try:
244248
args["assertion_consumer_service_url"] = kwargs[
245-
"assertion_consumer_service_url"]
246-
del kwargs["assertion_consumer_service_url"]
249+
"assertion_consumer_service_urls"][0]
250+
del kwargs["assertion_consumer_service_urls"]
247251
except KeyError:
248252
try:
249-
args["assertion_consumer_service_index"] = str(kwargs[
250-
"assertion_consumer_service_index"])
251-
del kwargs["assertion_consumer_service_index"]
253+
args["assertion_consumer_service_url"] = kwargs[
254+
"assertion_consumer_service_url"]
255+
del kwargs["assertion_consumer_service_url"]
252256
except KeyError:
253-
if service_url_binding is None:
254-
service_urls = self.service_urls(binding)
255-
else:
256-
service_urls = self.service_urls(service_url_binding)
257-
args["assertion_consumer_service_url"] = service_urls[0]
257+
try:
258+
args["assertion_consumer_service_index"] = str(
259+
kwargs["assertion_consumer_service_index"])
260+
del kwargs["assertion_consumer_service_index"]
261+
except KeyError:
262+
if service_url_binding is None:
263+
service_urls = self.service_urls(binding)
264+
else:
265+
service_urls = self.service_urls(service_url_binding)
266+
args["assertion_consumer_service_url"] = service_urls[0]
258267

259268
try:
260269
args["provider_name"] = kwargs["provider_name"]
@@ -268,7 +277,7 @@ def create_authn_request(self, destination, vorg="", scoping=None,
268277
# all of these have cardinality 0..1
269278
_msg = AuthnRequest()
270279
for param in ["scoping", "requested_authn_context", "conditions",
271-
"subject", "scoping"]:
280+
"subject"]:
272281
try:
273282
_item = kwargs[param]
274283
except KeyError:
@@ -288,23 +297,37 @@ def create_authn_request(self, destination, vorg="", scoping=None,
288297
args["name_id_policy"] = kwargs["name_id_policy"]
289298
del kwargs["name_id_policy"]
290299
except KeyError:
291-
if allow_create:
292-
allow_create = "true"
293-
else:
294-
allow_create = "false"
300+
if allow_create is None:
301+
allow_create = self.config.getattr("name_id_format_allow_create", "sp")
302+
if allow_create is None:
303+
allow_create = "false"
304+
else:
305+
if allow_create is True:
306+
allow_create = "true"
307+
else:
308+
allow_create = "false"
295309

296310
if nameid_format == "":
297311
name_id_policy = None
298312
else:
299313
if nameid_format is None:
300314
nameid_format = self.config.getattr("name_id_format", "sp")
301315

316+
# If no nameid_format has been set in the configuration
317+
# or passed in then transient is the default.
302318
if nameid_format is None:
303319
nameid_format = NAMEID_FORMAT_TRANSIENT
320+
321+
# If a list has been configured or passed in choose the
322+
# first since NameIDPolicy can only have one format specified.
304323
elif isinstance(nameid_format, list):
305-
# NameIDPolicy can only have one format specified
306324
nameid_format = nameid_format[0]
307325

326+
# Allow a deployer to signal that no format should be specified
327+
# in the NameIDPolicy by passing in or configuring the string 'None'.
328+
elif nameid_format == 'None':
329+
nameid_format = None
330+
308331
name_id_policy = samlp.NameIDPolicy(allow_create=allow_create,
309332
format=nameid_format)
310333

@@ -321,6 +344,75 @@ def create_authn_request(self, destination, vorg="", scoping=None,
321344
except KeyError:
322345
nsprefix = None
323346

347+
try:
348+
force_authn = kwargs['force_authn']
349+
except KeyError:
350+
force_authn = self.config.getattr('force_authn', 'sp')
351+
finally:
352+
if force_authn:
353+
args['force_authn'] = 'true'
354+
355+
conf_sp_type = self.config.getattr('sp_type', 'sp')
356+
conf_sp_type_in_md = self.config.getattr('sp_type_in_metadata', 'sp')
357+
if conf_sp_type and conf_sp_type_in_md is False:
358+
if not extensions:
359+
extensions = Extensions()
360+
item = sp_type.SPType(text=conf_sp_type)
361+
extensions.add_extension_element(item)
362+
363+
requested_attrs = self.config.getattr('requested_attributes', 'sp')
364+
if requested_attrs:
365+
if not extensions:
366+
extensions = Extensions()
367+
368+
attributemapsmods = []
369+
for modname in attributemaps.__all__:
370+
attributemapsmods.append(getattr(attributemaps, modname))
371+
372+
items = []
373+
for attr in requested_attrs:
374+
friendly_name = attr.get('friendly_name')
375+
name = attr.get('name')
376+
name_format = attr.get('name_format')
377+
is_required = str(attr.get('required', False)).lower()
378+
379+
if not name and not friendly_name:
380+
raise ValueError(
381+
"Missing required attribute: '{}' or '{}'".format(
382+
'name', 'friendly_name'))
383+
384+
if not name:
385+
for mod in attributemapsmods:
386+
try:
387+
name = mod.MAP['to'][friendly_name]
388+
except KeyError:
389+
continue
390+
else:
391+
if not name_format:
392+
name_format = mod.MAP['identifier']
393+
break
394+
395+
if not friendly_name:
396+
for mod in attributemapsmods:
397+
try:
398+
friendly_name = mod.MAP['fro'][name]
399+
except KeyError:
400+
continue
401+
else:
402+
if not name_format:
403+
name_format = mod.MAP['identifier']
404+
break
405+
406+
items.append(requested_attributes.RequestedAttribute(
407+
is_required=is_required,
408+
name_format=name_format,
409+
friendly_name=friendly_name,
410+
name=name))
411+
412+
item = requested_attributes.RequestedAttributes(
413+
extension_elements=items)
414+
extensions.add_extension_element(item)
415+
324416
if kwargs:
325417
_args, extensions = self._filter_args(AuthnRequest(), extensions,
326418
**kwargs)

src/saml2/config.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,14 @@
7373
"allow_unsolicited",
7474
"ecp",
7575
"name_id_format",
76+
"name_id_format_allow_create",
7677
"logout_requests_signed",
77-
"requested_attribute_name_format"
78+
"requested_attribute_name_format",
79+
"hide_assertion_consumer_service",
80+
"force_authn",
81+
"sp_type",
82+
"sp_type_in_metadata",
83+
"requested_attributes",
7884
]
7985

8086
AA_IDP_ARGS = [
@@ -187,6 +193,7 @@ def __init__(self, homedir="."):
187193
self.contact_person = None
188194
self.name_form = None
189195
self.name_id_format = None
196+
self.name_id_format_allow_create = None
190197
self.virtual_organization = None
191198
self.logger = None
192199
self.only_use_keys_in_metadata = True
@@ -205,7 +212,6 @@ def __init__(self, homedir="."):
205212
self.crypto_backend = 'xmlsec1'
206213
self.scope = ""
207214
self.allow_unknown_attributes = False
208-
self.allow_unsolicited = False
209215
self.extension_schema = {}
210216
self.cert_handler_extra_class = None
211217
self.verify_encrypt_cert_advice = None

0 commit comments

Comments
 (0)