~ddellav/ubuntu/wily/python-pysaml2/debian-merge

« back to all changes in this revision

Viewing changes to src/saml2/response.py

  • Committer: Package Import Robot
  • Author(s): Thomas Goirand
  • Date: 2014-09-08 16:11:53 UTC
  • Revision ID: package-import@ubuntu.com-20140908161153-vms9r4gu0oz4v4ai
Tags: upstream-2.0.0
ImportĀ upstreamĀ versionĀ 2.0.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
# -*- coding: utf-8 -*-
 
3
#
 
4
# Copyright (C) 2010-2011 UmeĆ„ University
 
5
#
 
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
 
9
#
 
10
#            http://www.apache.org/licenses/LICENSE-2.0
 
11
#
 
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.
 
17
 
 
18
import calendar
 
19
import logging
 
20
from saml2.samlp import STATUS_VERSION_MISMATCH
 
21
from saml2.samlp import STATUS_AUTHN_FAILED
 
22
from saml2.samlp import STATUS_INVALID_ATTR_NAME_OR_VALUE
 
23
from saml2.samlp import STATUS_INVALID_NAMEID_POLICY
 
24
from saml2.samlp import STATUS_NO_AUTHN_CONTEXT
 
25
from saml2.samlp import STATUS_NO_AVAILABLE_IDP
 
26
from saml2.samlp import STATUS_NO_PASSIVE
 
27
from saml2.samlp import STATUS_NO_SUPPORTED_IDP
 
28
from saml2.samlp import STATUS_PARTIAL_LOGOUT
 
29
from saml2.samlp import STATUS_PROXY_COUNT_EXCEEDED
 
30
from saml2.samlp import STATUS_REQUEST_DENIED
 
31
from saml2.samlp import STATUS_REQUEST_UNSUPPORTED
 
32
from saml2.samlp import STATUS_REQUEST_VERSION_DEPRECATED
 
33
from saml2.samlp import STATUS_REQUEST_VERSION_TOO_HIGH
 
34
from saml2.samlp import STATUS_REQUEST_VERSION_TOO_LOW
 
35
from saml2.samlp import STATUS_RESOURCE_NOT_RECOGNIZED
 
36
from saml2.samlp import STATUS_TOO_MANY_RESPONSES
 
37
from saml2.samlp import STATUS_UNKNOWN_ATTR_PROFILE
 
38
from saml2.samlp import STATUS_UNKNOWN_PRINCIPAL
 
39
from saml2.samlp import STATUS_UNSUPPORTED_BINDING
 
40
 
 
41
import xmldsig as ds
 
42
import xmlenc as xenc
 
43
 
 
44
from saml2 import samlp
 
45
from saml2 import saml
 
46
from saml2 import extension_element_to_element
 
47
from saml2 import extension_elements_to_elements
 
48
from saml2 import SAMLError
 
49
from saml2 import time_util
 
50
 
 
51
from saml2.s_utils import RequestVersionTooLow
 
52
from saml2.s_utils import RequestVersionTooHigh
 
53
from saml2.saml import attribute_from_string, XSI_TYPE
 
54
from saml2.saml import SCM_BEARER
 
55
from saml2.saml import SCM_HOLDER_OF_KEY
 
56
from saml2.saml import SCM_SENDER_VOUCHES
 
57
from saml2.saml import encrypted_attribute_from_string
 
58
from saml2.sigver import security_context
 
59
from saml2.sigver import SignatureError
 
60
from saml2.sigver import signed
 
61
from saml2.attribute_converter import to_local
 
62
from saml2.time_util import str_to_time, later_than
 
63
 
 
64
from saml2.validate import validate_on_or_after
 
65
from saml2.validate import validate_before
 
66
from saml2.validate import valid_instance
 
67
from saml2.validate import valid_address
 
68
from saml2.validate import NotValid
 
69
 
 
70
logger = logging.getLogger(__name__)
 
71
 
 
72
# ---------------------------------------------------------------------------
 
73
 
 
74
 
 
75
class IncorrectlySigned(SAMLError):
 
76
    pass
 
77
 
 
78
 
 
79
class DecryptionFailed(SAMLError):
 
80
    pass
 
81
 
 
82
 
 
83
class VerificationError(SAMLError):
 
84
    pass
 
85
 
 
86
 
 
87
class StatusError(SAMLError):
 
88
    pass
 
89
 
 
90
 
 
91
class UnsolicitedResponse(SAMLError):
 
92
    pass
 
93
 
 
94
 
 
95
class StatusVersionMismatch(StatusError):
 
96
    pass
 
97
 
 
98
 
 
99
class StatusAuthnFailed(StatusError):
 
100
    pass
 
101
 
 
102
 
 
103
class StatusInvalidAttrNameOrValue(StatusError):
 
104
    pass
 
105
 
 
106
 
 
107
class StatusInvalidNameidPolicy(StatusError):
 
108
    pass
 
109
 
 
110
 
 
111
class StatusNoAuthnContext(StatusError):
 
112
    pass
 
113
 
 
114
 
 
115
class StatusNoAvailableIdp(StatusError):
 
116
    pass
 
117
 
 
118
 
 
119
class StatusNoPassive(StatusError):
 
120
    pass
 
121
 
 
122
 
 
123
class StatusNoSupportedIdp(StatusError):
 
124
    pass
 
125
 
 
126
 
 
127
class StatusPartialLogout(StatusError):
 
128
    pass
 
129
 
 
130
 
 
131
class StatusProxyCountExceeded(StatusError):
 
132
    pass
 
133
 
 
134
 
 
135
class StatusRequestDenied(StatusError):
 
136
    pass
 
137
 
 
138
 
 
139
class StatusRequestUnsupported(StatusError):
 
140
    pass
 
141
 
 
142
 
 
143
class StatusRequestVersionDeprecated(StatusError):
 
144
    pass
 
145
 
 
146
 
 
147
class StatusRequestVersionTooHigh(StatusError):
 
148
    pass
 
149
 
 
150
 
 
151
class StatusRequestVersionTooLow(StatusError):
 
152
    pass
 
153
 
 
154
 
 
155
class StatusResourceNotRecognized(StatusError):
 
156
    pass
 
157
 
 
158
 
 
159
class StatusTooManyResponses(StatusError):
 
160
    pass
 
161
 
 
162
 
 
163
class StatusUnknownAttrProfile(StatusError):
 
164
    pass
 
165
 
 
166
 
 
167
class StatusUnknownPrincipal(StatusError):
 
168
    pass
 
169
 
 
170
 
 
171
class StatusUnsupportedBinding(StatusError):
 
172
    pass
 
173
 
 
174
 
 
175
STATUSCODE2EXCEPTION = {
 
176
    STATUS_VERSION_MISMATCH: StatusVersionMismatch,
 
177
    STATUS_AUTHN_FAILED: StatusAuthnFailed,
 
178
    STATUS_INVALID_ATTR_NAME_OR_VALUE: StatusInvalidAttrNameOrValue,
 
179
    STATUS_INVALID_NAMEID_POLICY: StatusInvalidNameidPolicy,
 
180
    STATUS_NO_AUTHN_CONTEXT: StatusNoAuthnContext,
 
181
    STATUS_NO_AVAILABLE_IDP: StatusNoAvailableIdp,
 
182
    STATUS_NO_PASSIVE: StatusNoPassive,
 
183
    STATUS_NO_SUPPORTED_IDP: StatusNoSupportedIdp,
 
184
    STATUS_PARTIAL_LOGOUT: StatusPartialLogout,
 
185
    STATUS_PROXY_COUNT_EXCEEDED: StatusProxyCountExceeded,
 
186
    STATUS_REQUEST_DENIED: StatusRequestDenied,
 
187
    STATUS_REQUEST_UNSUPPORTED: StatusRequestUnsupported,
 
188
    STATUS_REQUEST_VERSION_DEPRECATED: StatusRequestVersionDeprecated,
 
189
    STATUS_REQUEST_VERSION_TOO_HIGH: StatusRequestVersionTooHigh,
 
190
    STATUS_REQUEST_VERSION_TOO_LOW: StatusRequestVersionTooLow,
 
191
    STATUS_RESOURCE_NOT_RECOGNIZED: StatusResourceNotRecognized,
 
192
    STATUS_TOO_MANY_RESPONSES: StatusTooManyResponses,
 
193
    STATUS_UNKNOWN_ATTR_PROFILE: StatusUnknownAttrProfile,
 
194
    STATUS_UNKNOWN_PRINCIPAL: StatusUnknownPrincipal,
 
195
    STATUS_UNSUPPORTED_BINDING: StatusUnsupportedBinding,
 
196
}
 
197
# ---------------------------------------------------------------------------
 
198
 
 
199
 
 
200
def _dummy(_):
 
201
    return None
 
202
 
 
203
 
 
204
def for_me(conditions, myself):
 
205
    """ Am I among the intended audiences """
 
206
 
 
207
    if not conditions.audience_restriction: # No audience restriction
 
208
        return True
 
209
 
 
210
    for restriction in conditions.audience_restriction:
 
211
        if not restriction.audience:
 
212
            continue
 
213
        for audience in restriction.audience:
 
214
            if audience.text.strip() == myself:
 
215
                return True
 
216
            else:
 
217
                #print "Not for me: %s != %s" % (audience.text.strip(), myself)
 
218
                pass
 
219
    
 
220
    return False
 
221
 
 
222
 
 
223
def authn_response(conf, return_addrs, outstanding_queries=None, timeslack=0,
 
224
                   asynchop=True, allow_unsolicited=False, want_assertions_signed=False):
 
225
    sec = security_context(conf)
 
226
    if not timeslack:
 
227
        try:
 
228
            timeslack = int(conf.accepted_time_diff)
 
229
        except TypeError:
 
230
            timeslack = 0
 
231
    
 
232
    return AuthnResponse(sec, conf.attribute_converters, conf.entityid,
 
233
                         return_addrs, outstanding_queries, timeslack,
 
234
                         asynchop=asynchop, allow_unsolicited=allow_unsolicited,
 
235
                         want_assertions_signed=want_assertions_signed)
 
236
 
 
237
 
 
238
# comes in over SOAP so synchronous
 
239
def attribute_response(conf, return_addrs, timeslack=0, asynchop=False,
 
240
                       test=False):
 
241
    sec = security_context(conf)
 
242
    if not timeslack:
 
243
        try:
 
244
            timeslack = int(conf.accepted_time_diff)
 
245
        except TypeError:
 
246
            timeslack = 0
 
247
 
 
248
    return AttributeResponse(sec, conf.attribute_converters, conf.entityid,
 
249
                             return_addrs, timeslack, asynchop=asynchop,
 
250
                             test=test)
 
251
 
 
252
 
 
253
class StatusResponse(object):
 
254
    msgtype = "status_response"
 
255
 
 
256
    def __init__(self, sec_context, return_addrs=None, timeslack=0,
 
257
                 request_id=0, asynchop=True):
 
258
        self.sec = sec_context
 
259
        self.return_addrs = return_addrs
 
260
 
 
261
        self.timeslack = timeslack
 
262
        self.request_id = request_id
 
263
 
 
264
        self.xmlstr = ""
 
265
        self.name_id = None
 
266
        self.response = None
 
267
        self.not_on_or_after = 0
 
268
        self.in_response_to = None
 
269
        self.signature_check = self.sec.correctly_signed_response
 
270
        self.require_signature = False
 
271
        self.require_response_signature = False
 
272
        self.not_signed = False
 
273
        self.asynchop = asynchop
 
274
    
 
275
    def _clear(self):
 
276
        self.xmlstr = ""
 
277
        self.name_id = None
 
278
        self.response = None
 
279
        self.not_on_or_after = 0
 
280
        
 
281
    def _postamble(self):
 
282
        if not self.response:
 
283
            logger.error("Response was not correctly signed")
 
284
            if self.xmlstr:
 
285
                logger.info(self.xmlstr)
 
286
            raise IncorrectlySigned()
 
287
 
 
288
        logger.debug("response: %s" % (self.response,))
 
289
 
 
290
        try:
 
291
            valid_instance(self.response)
 
292
        except NotValid, exc:
 
293
            logger.error("Not valid response: %s" % exc.args[0])
 
294
            self._clear()
 
295
            return self
 
296
        
 
297
        self.in_response_to = self.response.in_response_to
 
298
        return self
 
299
        
 
300
    def load_instance(self, instance):
 
301
        if signed(instance):
 
302
            # This will check signature on Assertion which is the default
 
303
            try:
 
304
                self.response = self.sec.check_signature(instance)
 
305
            except SignatureError:
 
306
                # The response as a whole might be signed or not
 
307
                self.response = self.sec.check_signature(
 
308
                    instance, samlp.NAMESPACE + ":Response")
 
309
        else:
 
310
            self.not_signed = True
 
311
            self.response = instance
 
312
            
 
313
        return self._postamble()
 
314
        
 
315
    def _loads(self, xmldata, decode=True, origxml=None):
 
316
 
 
317
        # own copy
 
318
        self.xmlstr = xmldata[:]
 
319
        logger.debug("xmlstr: %s" % (self.xmlstr,))
 
320
 
 
321
        try:
 
322
            self.response = self.signature_check(xmldata, origdoc=origxml, must=self.require_signature,
 
323
                                                 require_response_signature=self.require_response_signature)
 
324
 
 
325
        except TypeError:
 
326
            raise
 
327
        except SignatureError:
 
328
            raise
 
329
        except Exception, excp:
 
330
            #logger.exception("EXCEPTION: %s", excp)
 
331
            raise
 
332
    
 
333
        #print "<", self.response
 
334
        
 
335
        return self._postamble()
 
336
    
 
337
    def status_ok(self):
 
338
        if self.response.status:
 
339
            status = self.response.status
 
340
            logger.info("status: %s" % (status,))
 
341
            if status.status_code.value != samlp.STATUS_SUCCESS:
 
342
                logger.info("Not successful operation: %s" % status)
 
343
                if status.status_code.status_code:
 
344
                    excep = STATUSCODE2EXCEPTION[
 
345
                        status.status_code.status_code.value]
 
346
                else:
 
347
                    excep = StatusError
 
348
                if status.status_message:
 
349
                    msg = status.status_message.text
 
350
                else:
 
351
                    try:
 
352
                        msg = status.status_code.status_code.value
 
353
                    except Exception:
 
354
                        msg = "Unknown error"
 
355
                raise excep(
 
356
                    "%s from %s" % (msg, status.status_code.value,))
 
357
        return True
 
358
 
 
359
    def issue_instant_ok(self):
 
360
        """ Check that the response was issued at a reasonable time """
 
361
        upper = time_util.shift_time(time_util.time_in_a_while(days=1),
 
362
                                     self.timeslack).timetuple()
 
363
        lower = time_util.shift_time(time_util.time_a_while_ago(days=1),
 
364
                                     -self.timeslack).timetuple()
 
365
        # print "issue_instant: %s" % self.response.issue_instant
 
366
        # print "%s < x < %s" % (lower, upper)
 
367
        issued_at = str_to_time(self.response.issue_instant)
 
368
        return lower < issued_at < upper
 
369
 
 
370
    def _verify(self):
 
371
        if self.request_id and self.in_response_to and \
 
372
                self.in_response_to != self.request_id:
 
373
            logger.error("Not the id I expected: %s != %s" % (
 
374
                self.in_response_to, self.request_id))
 
375
            return None
 
376
 
 
377
        try:
 
378
            assert self.response.version == "2.0"
 
379
        except AssertionError:
 
380
            _ver = float(self.response.version)
 
381
            if _ver < 2.0:
 
382
                raise RequestVersionTooLow()
 
383
            else:
 
384
                raise RequestVersionTooHigh()
 
385
 
 
386
        if self.asynchop:
 
387
            if self.response.destination and \
 
388
                    self.response.destination not in self.return_addrs:
 
389
                logger.error("%s not in %s" % (self.response.destination,
 
390
                                           self.return_addrs))
 
391
                return None
 
392
            
 
393
        assert self.issue_instant_ok()
 
394
        assert self.status_ok()
 
395
        return self
 
396
 
 
397
    def loads(self, xmldata, decode=True, origxml=None):
 
398
        return self._loads(xmldata, decode, origxml)
 
399
 
 
400
    def verify(self):
 
401
        try:
 
402
            return self._verify()
 
403
        except AssertionError:
 
404
            logger.exception("verify")
 
405
            return None
 
406
 
 
407
    def update(self, mold):
 
408
        self.xmlstr = mold.xmlstr
 
409
        self.in_response_to = mold.in_response_to
 
410
        self.response = mold.response
 
411
        
 
412
    def issuer(self):
 
413
        return self.response.issuer.text.strip()
 
414
        
 
415
 
 
416
class LogoutResponse(StatusResponse):
 
417
    msgtype = "logout_response"
 
418
 
 
419
    def __init__(self, sec_context, return_addrs=None, timeslack=0,
 
420
                 asynchop=True):
 
421
        StatusResponse.__init__(self, sec_context, return_addrs, timeslack,
 
422
                                asynchop=asynchop)
 
423
        self.signature_check = self.sec.correctly_signed_logout_response
 
424
 
 
425
 
 
426
class NameIDMappingResponse(StatusResponse):
 
427
    msgtype = "name_id_mapping_response"
 
428
 
 
429
    def __init__(self, sec_context, return_addrs=None, timeslack=0,
 
430
                 request_id=0, asynchop=True):
 
431
        StatusResponse.__init__(self, sec_context, return_addrs, timeslack,
 
432
                                request_id, asynchop)
 
433
        self.signature_check = self.sec.correctly_signed_name_id_mapping_response
 
434
 
 
435
 
 
436
class ManageNameIDResponse(StatusResponse):
 
437
    msgtype = "manage_name_id_response"
 
438
 
 
439
    def __init__(self, sec_context, return_addrs=None, timeslack=0,
 
440
                 request_id=0, asynchop=True):
 
441
        StatusResponse.__init__(self, sec_context, return_addrs, timeslack,
 
442
                                request_id, asynchop)
 
443
        self.signature_check = self.sec.correctly_signed_manage_name_id_response
 
444
 
 
445
 
 
446
# ----------------------------------------------------------------------------
 
447
 
 
448
 
 
449
class AuthnResponse(StatusResponse):
 
450
    """ This is where all the profile compliance is checked.
 
451
    This one does saml2int compliance. """
 
452
    msgtype = "authn_response"
 
453
 
 
454
    def __init__(self, sec_context, attribute_converters, entity_id,
 
455
                 return_addrs=None, outstanding_queries=None,
 
456
                 timeslack=0, asynchop=True, allow_unsolicited=False,
 
457
                 test=False, allow_unknown_attributes=False,
 
458
                 want_assertions_signed=False, want_response_signed=False, **kwargs):
 
459
 
 
460
        StatusResponse.__init__(self, sec_context, return_addrs, timeslack,
 
461
                                asynchop=asynchop)
 
462
        self.entity_id = entity_id
 
463
        self.attribute_converters = attribute_converters
 
464
        if outstanding_queries:
 
465
            self.outstanding_queries = outstanding_queries
 
466
        else:
 
467
            self.outstanding_queries = {}
 
468
        self.context = "AuthnReq"        
 
469
        self.came_from = ""
 
470
        self.ava = None
 
471
        self.assertion = None
 
472
        self.session_not_on_or_after = 0
 
473
        self.allow_unsolicited = allow_unsolicited
 
474
        self.require_signature = want_assertions_signed
 
475
        self.require_response_signature = want_response_signed
 
476
        self.test = test
 
477
        self.allow_unknown_attributes = allow_unknown_attributes
 
478
        #
 
479
        try:
 
480
            self.extension_schema = kwargs["extension_schema"]
 
481
        except KeyError:
 
482
            self.extension_schema = {}
 
483
 
 
484
    def loads(self, xmldata, decode=True, origxml=None):
 
485
        self._loads(xmldata, decode, origxml)
 
486
        
 
487
        if self.asynchop:
 
488
            if self.in_response_to in self.outstanding_queries:
 
489
                self.came_from = self.outstanding_queries[self.in_response_to]
 
490
                del self.outstanding_queries[self.in_response_to]
 
491
            elif self.allow_unsolicited:
 
492
                pass
 
493
            else:
 
494
                logger.exception("Unsolicited response %s" % self.in_response_to)
 
495
                raise UnsolicitedResponse("Unsolicited response: %s" % self.in_response_to)
 
496
            
 
497
        return self
 
498
 
 
499
    def clear(self):
 
500
        self._clear()
 
501
        self.came_from = ""
 
502
        self.ava = None
 
503
        self.assertion = None
 
504
        
 
505
    def authn_statement_ok(self, optional=False):
 
506
        try:
 
507
            # the assertion MUST contain one AuthNStatement
 
508
            assert len(self.assertion.authn_statement) == 1
 
509
        except AssertionError:
 
510
            if optional:
 
511
                return True
 
512
            else:
 
513
                raise
 
514
            
 
515
        authn_statement = self.assertion.authn_statement[0]
 
516
        if authn_statement.session_not_on_or_after:
 
517
            if validate_on_or_after(authn_statement.session_not_on_or_after,
 
518
                                    self.timeslack):
 
519
                self.session_not_on_or_after = calendar.timegm(
 
520
                    time_util.str_to_time(
 
521
                        authn_statement.session_not_on_or_after))
 
522
            else:
 
523
                return False
 
524
        return True
 
525
        # check authn_statement.session_index
 
526
    
 
527
    def condition_ok(self, lax=False):
 
528
        if self.test:
 
529
            lax = True
 
530
 
 
531
        # The Identity Provider MUST include a <saml:Conditions> element
 
532
        assert self.assertion.conditions
 
533
        conditions = self.assertion.conditions
 
534
 
 
535
        logger.debug("conditions: %s" % conditions)
 
536
 
 
537
        # if no sub-elements or elements are supplied, then the
 
538
        # assertion is considered to be valid.
 
539
        if not conditions.keyswv():
 
540
            return True
 
541
 
 
542
        # if both are present NotBefore must be earlier than NotOnOrAfter
 
543
        if conditions.not_before and conditions.not_on_or_after:
 
544
            if not later_than(conditions.not_on_or_after, conditions.not_before):
 
545
                return False
 
546
 
 
547
        try:
 
548
            if conditions.not_on_or_after:
 
549
                self.not_on_or_after = validate_on_or_after(
 
550
                    conditions.not_on_or_after, self.timeslack)
 
551
            if conditions.not_before:
 
552
                validate_before(conditions.not_before, self.timeslack)
 
553
        except Exception, excp:
 
554
            logger.error("Exception on conditions: %s" % (excp,))
 
555
            if not lax:
 
556
                raise
 
557
            else:
 
558
                self.not_on_or_after = 0
 
559
 
 
560
        if not self.allow_unsolicited:
 
561
            if not for_me(conditions, self.entity_id):
 
562
                if not lax:
 
563
                    raise Exception("Not for me!!!")
 
564
 
 
565
        if conditions.condition: # extra conditions
 
566
            for cond in conditions.condition:
 
567
                try:
 
568
                    if cond.extension_attributes[XSI_TYPE] in self.extension_schema:
 
569
                        pass
 
570
                    else:
 
571
                        raise Exception("Unknown condition")
 
572
                except KeyError:
 
573
                    raise Exception("Missing xsi:type specification")
 
574
 
 
575
        return True
 
576
 
 
577
    def decrypt_attributes(self, attribute_statement):
 
578
        """
 
579
        Decrypts possible encrypted attributes and adds the decrypts to the
 
580
        list of attributes.
 
581
 
 
582
        :param attribute_statement: A SAML.AttributeStatement which might
 
583
            contain both encrypted attributes and attributes.
 
584
        """
 
585
#        _node_name = [
 
586
#            "urn:oasis:names:tc:SAML:2.0:assertion:EncryptedData",
 
587
#            "urn:oasis:names:tc:SAML:2.0:assertion:EncryptedAttribute"]
 
588
 
 
589
        for encattr in attribute_statement.encrypted_attribute:
 
590
            if not encattr.encrypted_key:
 
591
                _decr = self.sec.decrypt(encattr.encrypted_data)
 
592
                _attr = attribute_from_string(_decr)
 
593
                attribute_statement.attribute.append(_attr)
 
594
            else:
 
595
                _decr = self.sec.decrypt(encattr)
 
596
                enc_attr = encrypted_attribute_from_string(_decr)
 
597
                attrlist = enc_attr.extensions_as_elements("Attribute", saml)
 
598
                attribute_statement.attribute.extend(attrlist)
 
599
 
 
600
    def get_identity(self):
 
601
        """ The assertion can contain zero or one attributeStatements
 
602
 
 
603
        """
 
604
        if not self.assertion.attribute_statement:
 
605
            logger.error("Missing Attribute Statement")
 
606
            ava = {}
 
607
        else:
 
608
            assert len(self.assertion.attribute_statement) == 1
 
609
            _attr_statem = self.assertion.attribute_statement[0]
 
610
 
 
611
            logger.debug("Attribute Statement: %s" % (_attr_statem,))
 
612
            for aconv in self.attribute_converters:
 
613
                logger.debug("Converts name format: %s" % (aconv.name_format,))
 
614
 
 
615
            self.decrypt_attributes(_attr_statem)
 
616
            ava = to_local(self.attribute_converters, _attr_statem,
 
617
                           self.allow_unknown_attributes)
 
618
        return ava
 
619
 
 
620
    def _bearer_confirmed(self, data):
 
621
        if not data:
 
622
            return False
 
623
 
 
624
        if data.address:
 
625
            if not valid_address(data.address):
 
626
                return False
 
627
            # verify that I got it from the correct sender
 
628
 
 
629
        # These two will raise exception if untrue
 
630
        validate_on_or_after(data.not_on_or_after, self.timeslack)
 
631
        validate_before(data.not_before, self.timeslack)
 
632
 
 
633
        # not_before must be < not_on_or_after
 
634
        if not later_than(data.not_on_or_after, data.not_before):
 
635
            return False
 
636
 
 
637
        if self.asynchop and not self.came_from:
 
638
            if data.in_response_to:
 
639
                if data.in_response_to in self.outstanding_queries:
 
640
                    self.came_from = self.outstanding_queries[
 
641
                        data.in_response_to]
 
642
                    del self.outstanding_queries[data.in_response_to]
 
643
                elif self.allow_unsolicited:
 
644
                    pass
 
645
                else:
 
646
                    # This is where I don't allow unsolicited reponses
 
647
                    # Either in_response_to == None or has a value I don't
 
648
                    # recognize
 
649
                    logger.debug("in response to: '%s'" % data.in_response_to)
 
650
                    logger.info("outstanding queries: %s" % (
 
651
                        self.outstanding_queries.keys(),))
 
652
                    raise Exception(
 
653
                        "Combination of session id and requestURI I don't recall")
 
654
        return True
 
655
 
 
656
    def _holder_of_key_confirmed(self, data):
 
657
        if not data:
 
658
            return False
 
659
 
 
660
        has_keyinfo = False
 
661
        for element in extension_elements_to_elements(data,
 
662
                                                      [samlp, saml, xenc, ds]):
 
663
            if isinstance(element, ds.KeyInfo):
 
664
                has_keyinfo = True
 
665
 
 
666
        return has_keyinfo
 
667
 
 
668
    def get_subject(self):
 
669
        """ The assertion must contain a Subject
 
670
        """
 
671
        assert self.assertion.subject
 
672
        subject = self.assertion.subject
 
673
        subjconf = []
 
674
        for subject_confirmation in subject.subject_confirmation:
 
675
            _data = subject_confirmation.subject_confirmation_data
 
676
 
 
677
            if subject_confirmation.method == SCM_BEARER:
 
678
                if not self._bearer_confirmed(_data):
 
679
                    continue
 
680
            elif subject_confirmation.method == SCM_HOLDER_OF_KEY:
 
681
                if not self._holder_of_key_confirmed(_data):
 
682
                    continue
 
683
            elif subject_confirmation.method == SCM_SENDER_VOUCHES:
 
684
                pass
 
685
            else:
 
686
                raise ValueError("Unknown subject confirmation method: %s" % (
 
687
                    subject_confirmation.method,))
 
688
 
 
689
            subjconf.append(subject_confirmation)
 
690
            
 
691
        if not subjconf:
 
692
            raise VerificationError("No valid subject confirmation")
 
693
            
 
694
        subject.subject_confirmation = subjconf
 
695
        
 
696
        # The subject must contain a name_id
 
697
        try:
 
698
            assert subject.name_id
 
699
            self.name_id = subject.name_id
 
700
        except AssertionError:
 
701
            if subject.encrypted_id:
 
702
                # decrypt encrypted ID
 
703
                _name_id_str = self.sec.decrypt(
 
704
                    subject.encrypted_id.encrypted_data.to_string())
 
705
                _name_id = saml.name_id_from_string(_name_id_str)
 
706
                self.name_id = _name_id
 
707
            else:
 
708
                raise VerificationError("Missing NameID")
 
709
 
 
710
        logger.info("Subject NameID: %s" % self.name_id)
 
711
        return self.name_id
 
712
    
 
713
    def _assertion(self, assertion):
 
714
        self.assertion = assertion
 
715
 
 
716
        logger.debug("assertion context: %s" % (self.context,))
 
717
        logger.debug("assertion keys: %s" % (assertion.keyswv()))
 
718
        logger.debug("outstanding_queries: %s" % (self.outstanding_queries,))
 
719
        
 
720
        #if self.context == "AuthnReq" or self.context == "AttrQuery":
 
721
        if self.context == "AuthnReq":
 
722
            self.authn_statement_ok()
 
723
#        elif self.context == "AttrQuery":
 
724
#            self.authn_statement_ok(True)
 
725
 
 
726
        if not self.condition_ok():
 
727
            raise VerificationError("Condition not OK")
 
728
 
 
729
        logger.debug("--- Getting Identity ---")
 
730
 
 
731
        if self.context == "AuthnReq" or self.context == "AttrQuery":
 
732
            self.ava = self.get_identity()
 
733
 
 
734
            logger.debug("--- AVA: %s" % (self.ava,))
 
735
        
 
736
        try:
 
737
            self.get_subject()
 
738
            if self.asynchop:
 
739
                if self.allow_unsolicited:
 
740
                    pass
 
741
                elif not self.came_from:
 
742
                    raise VerificationError("Came from")
 
743
            return True
 
744
        except Exception:
 
745
            logger.exception("get subject")
 
746
            raise
 
747
    
 
748
    def _encrypted_assertion(self, xmlstr):
 
749
        if xmlstr.encrypted_data:
 
750
            assertion_str = self.sec.decrypt(xmlstr.encrypted_data.to_string())
 
751
            if not assertion_str:
 
752
                raise DecryptionFailed()
 
753
            assertion = saml.assertion_from_string(assertion_str)
 
754
        else:
 
755
            decrypt_xml = self.sec.decrypt(xmlstr)
 
756
 
 
757
            logger.debug("Decryption successfull")
 
758
 
 
759
            self.response = samlp.response_from_string(decrypt_xml)
 
760
            logger.debug("Parsed decrypted assertion successfull")
 
761
 
 
762
            enc = self.response.encrypted_assertion[0].extension_elements[0]
 
763
            assertion = extension_element_to_element(
 
764
                enc, saml.ELEMENT_FROM_STRING, namespace=saml.NAMESPACE)
 
765
 
 
766
        logger.debug("Decrypted Assertion: %s" % assertion)
 
767
        return self._assertion(assertion)
 
768
    
 
769
    def parse_assertion(self):
 
770
        if self.context == "AuthnQuery":
 
771
            # can contain one or more assertions
 
772
            pass
 
773
        else:  # This is a saml2int limitation
 
774
            try:
 
775
                assert len(self.response.assertion) == 1 or \
 
776
                    len(self.response.encrypted_assertion) == 1
 
777
            except AssertionError:
 
778
                raise Exception("No assertion part")
 
779
        
 
780
        if self.response.assertion:
 
781
            logger.debug("***Unencrypted response***")
 
782
            for assertion in self.response.assertion:
 
783
                if not self._assertion(assertion):
 
784
                    return False
 
785
            return True
 
786
        else:
 
787
            logger.debug("***Encrypted response***")
 
788
            for assertion in self.response.encrypted_assertion:
 
789
                if not self._encrypted_assertion(assertion):
 
790
                    return False
 
791
            return True
 
792
 
 
793
    def verify(self):
 
794
        """ Verify that the assertion is syntactically correct and
 
795
        the signature is correct if present."""
 
796
        
 
797
        try:
 
798
            self._verify()
 
799
        except AssertionError:
 
800
            raise
 
801
 
 
802
        if not isinstance(self.response, samlp.Response):
 
803
            return self
 
804
 
 
805
        if self.parse_assertion():
 
806
            return self
 
807
        else:
 
808
            logger.error("Could not parse the assertion")
 
809
            return None
 
810
        
 
811
    def session_id(self):
 
812
        """ Returns the SessionID of the response """ 
 
813
        return self.response.in_response_to
 
814
    
 
815
    def id(self):
 
816
        """ Return the ID of the response """
 
817
        return self.response.id
 
818
    
 
819
    def authn_info(self):
 
820
        res = []
 
821
        for astat in self.assertion.authn_statement:
 
822
            context = astat.authn_context
 
823
            if context:
 
824
                try:
 
825
                    aclass = context.authn_context_class_ref.text
 
826
                except AttributeError:
 
827
                    aclass = ""
 
828
                try:
 
829
                    authn_auth = [a.text for a in
 
830
                                  context.authenticating_authority]
 
831
                except AttributeError:
 
832
                    authn_auth = []
 
833
                res.append((aclass, authn_auth))
 
834
        return res
 
835
 
 
836
    def authz_decision_info(self):
 
837
        res = {"permit": [], "deny": [], "indeterminate": []}
 
838
        for adstat in self.assertion.authz_decision_statement:
 
839
            # one of 'Permit', 'Deny', 'Indeterminate'
 
840
            res[adstat.decision.text.lower()] = adstat
 
841
        return res
 
842
 
 
843
    def session_info(self):
 
844
        """ Returns a predefined set of information gleened from the 
 
845
        response.
 
846
        :returns: Dictionary with information
 
847
        """
 
848
        if self.session_not_on_or_after > 0:
 
849
            nooa = self.session_not_on_or_after
 
850
        else:
 
851
            nooa = self.not_on_or_after
 
852
 
 
853
        if self.context == "AuthzQuery":
 
854
            return {"name_id": self.name_id, "came_from": self.came_from,
 
855
                    "issuer": self.issuer(), "not_on_or_after": nooa,
 
856
                    "authz_decision_info": self.authz_decision_info()}
 
857
        else:
 
858
            return {"ava": self.ava, "name_id": self.name_id,
 
859
                    "came_from": self.came_from, "issuer": self.issuer(),
 
860
                    "not_on_or_after": nooa, "authn_info": self.authn_info()}
 
861
    
 
862
    def __str__(self):
 
863
        return "%s" % self.xmlstr
 
864
 
 
865
    def verify_attesting_entity(self, address):
 
866
        """
 
867
        Assumes one assertion. At least one address specification has to be
 
868
        correct.
 
869
 
 
870
        :param address: IP address of attesting entity
 
871
        :return: True/False
 
872
        """
 
873
 
 
874
        correct = 0
 
875
        for subject_conf in self.assertion.subject.subject_confirmation:
 
876
            if subject_conf.subject_confirmation_data is None:
 
877
                correct += 1  # In reality undefined
 
878
            elif subject_conf.subject_confirmation_data.address:
 
879
                if subject_conf.subject_confirmation_data.address == address:
 
880
                    correct += 1
 
881
            else:
 
882
                correct += 1
 
883
 
 
884
        if correct:
 
885
            return True
 
886
        else:
 
887
            return False
 
888
 
 
889
 
 
890
class AuthnQueryResponse(AuthnResponse):
 
891
    msgtype = "authn_query_response"
 
892
 
 
893
    def __init__(self, sec_context, attribute_converters, entity_id,
 
894
                 return_addrs=None, timeslack=0, asynchop=False, test=False):
 
895
 
 
896
        AuthnResponse.__init__(self, sec_context, attribute_converters,
 
897
                               entity_id, return_addrs, timeslack=timeslack,
 
898
                               asynchop=asynchop, test=test)
 
899
        self.entity_id = entity_id
 
900
        self.attribute_converters = attribute_converters
 
901
        self.assertion = None
 
902
        self.context = "AuthnQuery"
 
903
 
 
904
    def condition_ok(self, lax=False):  # Should I care about conditions ?
 
905
        return True
 
906
 
 
907
 
 
908
class AttributeResponse(AuthnResponse):
 
909
    msgtype = "attribute_response"
 
910
 
 
911
    def __init__(self, sec_context, attribute_converters, entity_id,
 
912
                 return_addrs=None, timeslack=0, asynchop=False, test=False):
 
913
 
 
914
        AuthnResponse.__init__(self, sec_context, attribute_converters,
 
915
                               entity_id, return_addrs, timeslack=timeslack,
 
916
                               asynchop=asynchop, test=test)
 
917
        self.entity_id = entity_id
 
918
        self.attribute_converters = attribute_converters
 
919
        self.assertion = None
 
920
        self.context = "AttrQuery"
 
921
 
 
922
 
 
923
class AuthzResponse(AuthnResponse):
 
924
    """ A successful response will be in the form of assertions containing
 
925
    authorization decision statements."""
 
926
    msgtype = "authz_decision_response"
 
927
 
 
928
    def __init__(self, sec_context, attribute_converters, entity_id,
 
929
                 return_addrs=None, timeslack=0, asynchop=False):
 
930
        AuthnResponse.__init__(self, sec_context, attribute_converters,
 
931
                               entity_id, return_addrs, timeslack=timeslack,
 
932
                               asynchop=asynchop)
 
933
        self.entity_id = entity_id
 
934
        self.attribute_converters = attribute_converters
 
935
        self.assertion = None
 
936
        self.context = "AuthzQuery"
 
937
 
 
938
 
 
939
class ArtifactResponse(AuthnResponse):
 
940
    msgtype = "artifact_response"
 
941
 
 
942
    def __init__(self, sec_context, attribute_converters, entity_id,
 
943
                 return_addrs=None, timeslack=0, asynchop=False, test=False):
 
944
 
 
945
        AuthnResponse.__init__(self, sec_context, attribute_converters,
 
946
                               entity_id, return_addrs, timeslack=timeslack,
 
947
                               asynchop=asynchop, test=test)
 
948
        self.entity_id = entity_id
 
949
        self.attribute_converters = attribute_converters
 
950
        self.assertion = None
 
951
        self.context = "ArtifactResolve"
 
952
 
 
953
 
 
954
def response_factory(xmlstr, conf, return_addrs=None, outstanding_queries=None,
 
955
                     timeslack=0, decode=True, request_id=0, origxml=None,
 
956
                     asynchop=True, allow_unsolicited=False, want_assertions_signed=False):
 
957
    sec_context = security_context(conf)
 
958
    if not timeslack:
 
959
        try:
 
960
            timeslack = int(conf.accepted_time_diff)
 
961
        except TypeError:
 
962
            timeslack = 0
 
963
            
 
964
    attribute_converters = conf.attribute_converters
 
965
    entity_id = conf.entityid
 
966
    extension_schema = conf.extension_schema
 
967
 
 
968
    response = StatusResponse(sec_context, return_addrs, timeslack, request_id,
 
969
                              asynchop)
 
970
    try:
 
971
        response.loads(xmlstr, decode, origxml)
 
972
        if response.response.assertion or response.response.encrypted_assertion:
 
973
            authnresp = AuthnResponse(sec_context, attribute_converters,
 
974
                                      entity_id, return_addrs,
 
975
                                      outstanding_queries, timeslack, asynchop,
 
976
                                      allow_unsolicited,
 
977
                                      extension_schema=extension_schema,
 
978
                                      want_assertions_signed=want_assertions_signed)
 
979
            authnresp.update(response)
 
980
            return authnresp
 
981
    except TypeError:
 
982
        response.signature_check = sec_context.correctly_signed_logout_response
 
983
        response.loads(xmlstr, decode, origxml)
 
984
        logoutresp = LogoutResponse(sec_context, return_addrs, timeslack,
 
985
                                    asynchop=asynchop)
 
986
        logoutresp.update(response)
 
987
        return logoutresp
 
988
        
 
989
    return response
 
990
 
 
991
# ===========================================================================
 
992
# A class of it's own
 
993
 
 
994
 
 
995
class AssertionIDResponse(object):
 
996
    msgtype = "assertion_id_response"
 
997
 
 
998
    def __init__(self, sec_context, attribute_converters, timeslack=0,
 
999
                 **kwargs):
 
1000
 
 
1001
        self.sec = sec_context
 
1002
        self.timeslack = timeslack
 
1003
        self.xmlstr = ""
 
1004
        self.name_id = ""
 
1005
        self.response = None
 
1006
        self.not_signed = False
 
1007
        self.attribute_converters = attribute_converters
 
1008
        self.assertion = None
 
1009
        self.context = "AssertionIdResponse"
 
1010
        self.signature_check = self.sec.correctly_signed_assertion_id_response
 
1011
 
 
1012
    def loads(self, xmldata, decode=True, origxml=None):
 
1013
        # own copy
 
1014
        self.xmlstr = xmldata[:]
 
1015
        logger.debug("xmlstr: %s" % (self.xmlstr,))
 
1016
 
 
1017
        try:
 
1018
            self.response = self.signature_check(xmldata, origdoc=origxml)
 
1019
            self.assertion = self.response
 
1020
        except TypeError:
 
1021
            raise
 
1022
        except SignatureError:
 
1023
            raise
 
1024
        except Exception, excp:
 
1025
            logger.exception("EXCEPTION: %s", excp)
 
1026
            raise
 
1027
 
 
1028
        #print "<", self.response
 
1029
 
 
1030
        return self._postamble()
 
1031
 
 
1032
    def verify(self):
 
1033
        try:
 
1034
            valid_instance(self.response)
 
1035
        except NotValid, exc:
 
1036
            logger.error("Not valid response: %s" % exc.args[0])
 
1037
            raise
 
1038
        return self
 
1039
 
 
1040
    def _postamble(self):
 
1041
        if not self.response:
 
1042
            logger.error("Response was not correctly signed")
 
1043
            if self.xmlstr:
 
1044
                logger.info(self.xmlstr)
 
1045
            raise IncorrectlySigned()
 
1046
 
 
1047
        logger.debug("response: %s" % (self.response,))
 
1048
 
 
1049
        return self
 
1050