2
# -*- coding: utf-8 -*-
4
# Copyright (C) 2010-2011 UmeƄ University
6
# Licensed under the Apache License, Version 2.0 (the "License");
7
# you may not use this file except in compliance with the License.
8
# You may obtain a copy of the License at
10
# http://www.apache.org/licenses/LICENSE-2.0
12
# Unless required by applicable law or agreed to in writing, software
13
# distributed under the License is distributed on an "AS IS" BASIS,
14
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
# See the License for the specific language governing permissions and
16
# limitations under the License.
21
from saml2.saml import NAME_FORMAT_URI
24
from saml2 import saml
26
from saml2.time_util import instant, in_a_while
27
from saml2.attribute_converter import from_local
28
from saml2.s_utils import sid, MissingValue
29
from saml2.s_utils import factory
30
from saml2.s_utils import assertion_factory
33
logger = logging.getLogger(__name__)
36
def _filter_values(vals, vlist=None, must=False):
37
""" Removes values from *vals* that does not appear in vlist
39
:param vals: The values that are to be filtered
40
:param vlist: required or optional value
41
:param must: Whether the allowed values must appear
42
:return: The set of values after filtering
45
if not vlist: # No value specified equals any value
48
if isinstance(vlist, basestring):
61
raise MissingValue("Required attribute value missing")
66
def _match(attr, ava):
74
for _at in ava.keys():
75
if _at.lower() == _la:
81
def filter_on_attributes(ava, required=None, optional=None):
84
:param ava: An attribute value assertion as a dictionary
85
:param required: list of RequestedAttribute instances defined to be
87
:param optional: list of RequestedAttribute instances defined to be
89
:return: The modified attribute value assertion
99
for nform in ["friendly_name", "name"]:
101
_fn = _match(attr[nform], ava)
107
values = [av["text"] for av in attr["attribute_value"]]
110
res[_fn] = _filter_values(ava[_fn], values, True)
115
raise MissingValue("Required attribute missing: '%s'" % (
121
for attr in optional:
122
for nform in ["friendly_name", "name"]:
124
_fn = _match(attr[nform], ava)
127
values = [av["text"] for av in attr["attribute_value"]]
131
res[_fn].extend(_filter_values(ava[_fn], values))
133
res[_fn] = _filter_values(ava[_fn], values)
138
def filter_on_demands(ava, required=None, optional=None):
139
""" Never return more than is needed. Filters out everything
140
the server is prepared to return but the receiver doesn't ask for
142
:param ava: Attribute value assertion as a dictionary
143
:param required: Required attributes
144
:param optional: Optional attributes
145
:return: The possibly reduced assertion
148
# Is all what's required there:
152
lava = dict([(k.lower(), k) for k in ava.keys()])
154
for attr, vals in required.items():
159
if val not in ava[lava[attr]]:
161
"Required attribute value missing: %s,%s" % (attr,
164
raise MissingValue("Required attribute missing: %s" % (attr,))
169
oka = [k.lower() for k in required.keys()]
170
oka.extend([k.lower() for k in optional.keys()])
172
# OK, so I can imaging releasing values that are not absolutely necessary
173
# but not attributes that are not asked for.
174
for attr in lava.keys():
181
def filter_on_wire_representation(ava, acs, required=None, optional=None):
183
:param ava: A dictionary with attributes and values
184
:param acs: List of tuples (Attribute Converter name,
185
Attribute Converter instance)
186
:param required: A list of saml.Attributes
187
:param optional: A list of saml.Attributes
188
:return: Dictionary of expected/wanted attributes and values
190
acsdic = dict([(ac.name_format, ac) for ac in acs])
198
for attr, val in ava.items():
202
_name = acsdic[req.name_format]._to[attr]
203
if _name == req.name:
212
_name = acsdic[opt.name_format]._to[attr]
213
if _name == opt.name:
222
def filter_attribute_value_assertions(ava, attribute_restrictions=None):
223
""" Will weed out attribute values and values according to the
224
rules defined in the attribute restrictions. If filtering results in
225
an attribute without values, then the attribute is removed from the
228
:param ava: The incoming attribute value assertion (dictionary)
229
:param attribute_restrictions: The rules that govern which attributes
230
and values that are allowed. (dictionary)
231
:return: The modified attribute value assertion
233
if not attribute_restrictions:
236
for attr, vals in ava.items():
239
_rests = attribute_restrictions[_attr]
245
if isinstance(vals, basestring):
254
ava[attr] = list(set(rvals))
260
def restriction_from_attribute_spec(attributes):
262
for attribute in attributes:
263
restr[attribute.name] = {}
264
for val in attribute.attribute_value:
266
restr[attribute.name] = None
269
restr[attribute.name] = re.compile(val.text)
273
def post_entity_categories(maps, **kwargs):
277
ecs = kwargs["mds"].entity_categories(kwargs["sp_entity_id"])
280
for attr in ec_map[""]:
281
restrictions[attr] = None
284
for key, val in ec_map.items():
285
if key == "": # always released
287
elif isinstance(key, tuple):
292
except AssertionError:
301
restrictions[attr] = None
306
class Policy(object):
307
""" handles restrictions on assertions """
309
def __init__(self, restrictions=None):
311
self.compile(restrictions)
313
self._restrictions = None
315
def compile(self, restrictions):
316
""" This is only for IdPs or AAs, and it's about limiting what
317
is returned to the SP.
318
In the configuration file, restrictions on which values that
319
can be returned are specified with the help of regular expressions.
320
This function goes through and pre-compiles the regular expressions.
323
:return: The assertion with the string specification replaced with
324
a compiled regular expression.
327
self._restrictions = restrictions.copy()
329
for who, spec in self._restrictions.items():
333
items = spec["entity_categories"]
339
_mod = importlib.import_module(
340
"saml2.entity_category.%s" % cat)
342
for key, items in _mod.RELEASE.items():
343
_ec[key] = [k.lower() for k in items]
345
spec["entity_categories"] = ecs
347
restr = spec["attribute_restrictions"]
355
for key, values in restr.items():
357
_are[key.lower()] = None
360
_are[key.lower()] = [re.compile(value) for value in values]
361
spec["attribute_restrictions"] = _are
362
logger.debug("policy restrictions: %s" % self._restrictions)
364
return self._restrictions
366
def get(self, attribute, sp_entity_id, default=None, post_func=None,
376
if not self._restrictions:
381
val = self._restrictions[sp_entity_id][attribute]
384
val = self._restrictions["default"][attribute]
393
return post_func(val, sp_entity_id=sp_entity_id, **kwargs)
397
def get_nameid_format(self, sp_entity_id):
398
""" Get the NameIDFormat to used for the entity id
399
:param: The SP entity ID
402
return self.get("nameid_format", sp_entity_id,
403
saml.NAMEID_FORMAT_TRANSIENT)
405
def get_name_form(self, sp_entity_id):
406
""" Get the NameFormat to used for the entity id
407
:param: The SP entity ID
411
return self.get("name_format", sp_entity_id, NAME_FORMAT_URI)
413
def get_lifetime(self, sp_entity_id):
414
""" The lifetime of the assertion
415
:param sp_entity_id: The SP entity ID
416
:param: lifetime as a dictionary
419
return self.get("lifetime", sp_entity_id, {"hours": 1})
421
def get_attribute_restrictions(self, sp_entity_id):
422
""" Return the attribute restriction for SP that want the information
424
:param sp_entity_id: The SP entity ID
425
:return: The restrictions
428
return self.get("attribute_restrictions", sp_entity_id)
430
def entity_category_attributes(self, ec):
431
if not self._restrictions:
434
ec_maps = self._restrictions["default"]["entity_categories"]
435
for ec_map in ec_maps:
442
def get_entity_categories(self, sp_entity_id, mds):
446
:param mds: MetadataStore instance
447
:return: A dictionary with restrictions
450
kwargs = {"mds": mds}
452
return self.get("entity_categories", sp_entity_id, default={},
453
post_func=post_entity_categories, **kwargs)
455
def not_on_or_after(self, sp_entity_id):
456
""" When the assertion stops being valid, should not be
457
used after this time.
459
:param sp_entity_id: The SP entity ID
460
:return: String representation of the time
463
return in_a_while(**self.get_lifetime(sp_entity_id))
465
def filter(self, ava, sp_entity_id, mdstore, required=None, optional=None):
466
""" What attribute and attribute values returns depends on what
467
the SP has said it wants in the request or in the metadata file and
468
what the IdP/AA wants to release. An assumption is that what the SP
469
asks for overrides whatever is in the metadata. But of course the
470
IdP never releases anything it doesn't want to.
472
:param ava: The information about the subject as a dictionary
473
:param sp_entity_id: The entity ID of the SP
474
:param mdstore: A Metadata store
475
:param required: Attributes that the SP requires in the assertion
476
:param optional: Attributes that the SP regards as optional
477
:return: A possibly modified AVA
480
_rest = self.get_attribute_restrictions(sp_entity_id)
482
_rest = self.get_entity_categories(sp_entity_id, mdstore)
483
logger.debug("filter based on: %s" % _rest)
484
ava = filter_attribute_value_assertions(ava, _rest)
486
if required or optional:
487
ava = filter_on_attributes(ava, required, optional)
491
def restrict(self, ava, sp_entity_id, metadata=None):
492
""" Identity attribute names are expected to be expressed in
493
the local lingo (== friendlyName)
495
:return: A filtered ava according to the IdPs/AAs rules and
496
the list of required/optional attributes according to the SP.
497
If the requirements can't be met an exception is raised.
500
spec = metadata.attribute_requirement(sp_entity_id)
502
ava = self.filter(ava, sp_entity_id, metadata,
503
spec["required"], spec["optional"])
505
return self.filter(ava, sp_entity_id, metadata, [], [])
507
def conditions(self, sp_entity_id):
508
""" Return a saml.Condition instance
510
:param sp_entity_id: The SP entity ID
511
:return: A saml.Condition instance
513
return factory(saml.Conditions,
514
not_before=instant(),
515
# How long might depend on who's getting it
516
not_on_or_after=self.not_on_or_after(sp_entity_id),
517
audience_restriction=[factory(
518
saml.AudienceRestriction,
519
audience=[factory(saml.Audience,
520
text=sp_entity_id)])])
522
def get_sign(self, sp_entity_id):
525
"sign": ["response", "assertion", "on_demand"]
531
return self.get("sign", sp_entity_id, [])
534
class EntityCategories(object):
538
class Assertion(dict):
539
""" Handles assertions about subjects """
541
def __init__(self, dic=None):
542
dict.__init__(self, dic)
544
def _authn_context_decl(self, decl, authn_auth=None):
546
Construct the authn context with a authn context declaration
547
:param decl: The authn context declaration
548
:param authn_auth: Authenticating Authority
549
:return: An AuthnContext instance
551
return factory(saml.AuthnContext,
552
authn_context_decl=decl,
553
authenticating_authority=factory(
554
saml.AuthenticatingAuthority, text=authn_auth))
556
def _authn_context_decl_ref(self, decl_ref, authn_auth=None):
558
Construct the authn context with a authn context declaration reference
559
:param decl_ref: The authn context declaration reference
560
:param authn_auth: Authenticating Authority
561
:return: An AuthnContext instance
563
return factory(saml.AuthnContext,
564
authn_context_decl_ref=decl_ref,
565
authenticating_authority=factory(
566
saml.AuthenticatingAuthority, text=authn_auth))
569
def _authn_context_class_ref(authn_class, authn_auth=None):
571
Construct the authn context with a authn context class reference
572
:param authn_class: The authn context class reference
573
:param authn_auth: Authenticating Authority
574
:return: An AuthnContext instance
576
cntx_class = factory(saml.AuthnContextClassRef, text=authn_class)
578
return factory(saml.AuthnContext,
579
authn_context_class_ref=cntx_class,
580
authenticating_authority=factory(
581
saml.AuthenticatingAuthority, text=authn_auth))
583
return factory(saml.AuthnContext,
584
authn_context_class_ref=cntx_class)
586
def _authn_statement(self, authn_class=None, authn_auth=None,
587
authn_decl=None, authn_decl_ref=None, authn_instant="",
588
subject_locality=""):
590
Construct the AuthnStatement
591
:param authn_class: Authentication Context Class reference
592
:param authn_auth: Authenticating Authority
593
:param authn_decl: Authentication Context Declaration
594
:param authn_decl_ref: Authentication Context Declaration reference
595
:param authn_instant: When the Authentication was performed.
596
Assumed to be seconds since the Epoch.
597
:param subject_locality: Specifies the DNS domain name and IP address
598
for the system from which the assertion subject was apparently
600
:return: An AuthnContext instance
603
_instant = instant(time_stamp=authn_instant)
610
authn_instant=_instant,
612
authn_context=self._authn_context_class_ref(
613
authn_class, authn_auth))
617
authn_instant=_instant,
619
authn_context=self._authn_context_decl(authn_decl, authn_auth))
623
authn_instant=_instant,
625
authn_context=self._authn_context_decl_ref(authn_decl_ref,
630
authn_instant=_instant,
634
res.subject_locality = saml.SubjectLocality(text=subject_locality)
638
def construct(self, sp_entity_id, in_response_to, consumer_url,
639
name_id, attrconvs, policy, issuer, authn_class=None,
640
authn_auth=None, authn_decl=None, encrypt=None,
641
sec_context=None, authn_decl_ref=None, authn_instant="",
642
subject_locality=""):
643
""" Construct the Assertion
645
:param sp_entity_id: The entityid of the SP
646
:param in_response_to: An identifier of the message, this message is
648
:param consumer_url: The intended consumer of the assertion
649
:param name_id: An NameID instance
650
:param attrconvs: AttributeConverters
651
:param policy: The policy that should be adhered to when replying
652
:param issuer: Who is issuing the statement
653
:param authn_class: The authentication class
654
:param authn_auth: The authentication instance
655
:param authn_decl: An Authentication Context declaration
656
:param encrypt: Whether to encrypt parts or all of the Assertion
657
:param sec_context: The security context used when encrypting
658
:param authn_decl_ref: An Authentication Context declaration reference
659
:param authn_instant: When the Authentication was performed
660
:param subject_locality: Specifies the DNS domain name and IP address
661
for the system from which the assertion subject was apparently
663
:return: An Assertion instance
667
_name_format = policy.get_name_form(sp_entity_id)
669
_name_format = NAME_FORMAT_URI
671
attr_statement = saml.AttributeStatement(attribute=from_local(
672
attrconvs, self, _name_format))
674
if encrypt == "attributes":
675
for attr in attr_statement.attribute:
676
enc = sec_context.encrypt(text="%s" % attr)
678
encd = xmlenc.encrypted_data_from_string(enc)
679
encattr = saml.EncryptedAttribute(encrypted_data=encd)
680
attr_statement.encrypted_attribute.append(encattr)
682
attr_statement.attribute = []
684
# start using now and for some time
685
conds = policy.conditions(sp_entity_id)
687
if authn_auth or authn_class or authn_decl or authn_decl_ref:
688
_authn_statement = self._authn_statement(authn_class, authn_auth,
689
authn_decl, authn_decl_ref,
693
_authn_statement = None
696
_ass = assertion_factory(
702
subject_confirmation=[factory(
703
saml.SubjectConfirmation,
704
method=saml.SCM_BEARER,
705
subject_confirmation_data=factory(
706
saml.SubjectConfirmationData,
707
in_response_to=in_response_to,
708
recipient=consumer_url,
709
not_on_or_after=policy.not_on_or_after(sp_entity_id)))]
714
_ass.authn_statement = [_authn_statement]
716
if not attr_statement.empty():
717
_ass.attribute_statement=[attr_statement]
721
def apply_policy(self, sp_entity_id, policy, metadata=None):
722
""" Apply policy to the assertion I'm representing
724
:param sp_entity_id: The SP entity ID
725
:param policy: The policy
726
:param metadata: Metadata to use
727
:return: The resulting AVA after the policy is applied
729
ava = policy.restrict(self, sp_entity_id, metadata)
b'\\ No newline at end of file'