Changeset 1264

Show
Ignore:
Timestamp:
02/13/08 12:33:38 (11 months ago)
Author:
dbuss
Message:

#346 Added base documentation, enhanced a number of events to be more descriptive and added some event logging. Updated sample.

Location:
trunk/rp/common/python
Files:
6 added
5 modified

Legend:

Unmodified
Added
Removed
  • trunk/rp/common/python/infocard/event.py

    r1231 r1264  
    1 #  Copyright (c) 2007 Novell, Inc. 
     1#  Copyright (c) 2007, 2008 Novell, Inc. 
    22#  All Rights Reserved. 
    33  
     
    88#  This library is distributed in the hope that it will be useful, 
    99#  but WITHOUT ANY WARRANTY; without even the implied warranty of 
    10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
     10#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
    1111#  GNU Lesser General Public License for more details. 
    1212  
     
    2828NOTSET = logging.NOTSET 
    2929 
    30 config_fields = ['CRITICAL', 'DEBUG', 'ERROR', 'FATAL', 'INFO'] 
    31  
    32 class IEventLog: 
    33         """An interface for managing related events, including inline filtering 
    34         """ 
    35          
    36          
    37         def add_event( eid, text, level): 
    38                 """Add an event to the queue, filtering as needed 
    39                 """ 
    40                  
     30config_fields = ['CRITICAL', 'DEBUG', 'ERROR', 'FATAL', 'INFO', 'NOTSET'] 
    4131 
    4232class BasicEventLog: 
    43         """ Implementation of a basic event log. 
     33    """ Implementation of a basic event log. 
    4434     
    4535    This event log is not intended to be a process level event log, rather it is 
     
    4737    This event log allows specific tags to be upgraded or downgraded in severity 
    4838    based on configuration 
     39    """ 
     40 
     41    def __init__(self, options=None): 
     42        """ Process options for basic event log. 
     43         
     44        Currently the only options are for overriding severities based on  
     45        the event tag.   To configure a dictionary where the name of the config_fields 
     46        is the key name and the event tags are the value.  Event tags are space seperated. 
     47        ({'INFO' : 'parse-locate-encrypted signature-missing-InclusiveNameSpaces'}, 
     48                                                {'NOTSET' : 'bogus-message'}) 
     49         
     50        """ 
     51        self.events = [] #array of errors, warnings, and other significant events 
     52        self.options = {} 
     53        if options: 
     54            for field in config_fields: 
     55                if options.has_key(field): 
     56                    self.options[field] = options[field] 
     57#                   self.events.append(Event("setting %s to %s" % (field, options[field]))) 
     58 
     59    def _get_severity(self, severity, tag): 
     60        """ Internal routine to see if the tag has a configured override for it's 
     61        severity.    
     62         
     63        Returns the configured severity if set, otherwise returns the passed  
     64        severity.""" 
     65        if tag: 
     66            for field in config_fields: 
     67                if self.options.has_key(field) and (re.search(tag, self.options[field]) is not None): 
     68                    mapped_severity = logging.getLevelName(field) 
     69#                   self.events.append(Event("tag %s has severity %s instead of %s" % (tag, field, logging.getLevelName(severity)))) 
     70                    return mapped_severity 
     71        return severity 
     72 
     73    def add_event(self, text, severity=NOTSET, tag=None): 
     74        """Add an event to the event queue, may upgrade or downgrade the  
     75        severity, depending on the currently active options.""" 
     76         
     77        event = Event(text, self._get_severity(severity, tag), tag) 
     78        self.events.append(event) 
     79         
     80    def has_severity(self, severity): 
     81        """Get a count of the number of items of equal or greater than the passed 
     82        severity, with CRITICAL being the largest, and NOTSET being the lowest. 
     83         
     84        Useful to check and see if any actual errors occured, in contrast to  
     85        informational events. 
     86        """ 
     87        count = 0 
     88        for event in self.events: 
     89            if event.severity >= severity: 
     90                count += 1 
     91#       self.events.append(Event("examined %d events and found %d events of %s or greater" % (len(self.events), count, logging.getLevelName(severity)))) 
     92        return count 
     93             
     94class Event: 
     95    """Object to hold an indivdual event. 
    4996     
    50         """ 
    51         config_fields = ['CRITICAL', 'DEBUG', 'ERROR', 'FATAL', 'INFO'] 
    52         def __init__(self, options=None): 
    53                 """ 
    54                 """ 
    55                 self.events = [] #array of errors, warnings, and other significant events 
    56                 self.options = {} 
    57                 if options: 
    58                         for field in config_fields: 
    59                                 if options.has_key(field): 
    60                                         self.options[field] = options[field] 
    61 #                                       self.events.append(Event("setting %s to %s" % (field, options[field]))) 
    62  
    63         def _get_severity(self, severity, tag): 
    64                 if tag: 
    65                         for field in config_fields: 
    66                                 if self.options.has_key(field) and (re.search(tag, self.options[field]) is not None): 
    67                                         mapped_severity = logging.getLevelName(field) 
    68 #                                       self.events.append(Event("tag %s has severity %s instead of %s" % (tag, field, logging.getLevelName(severity)))) 
    69                                         return mapped_severity 
    70                 return severity 
    71  
    72         def add_event(self, text, severity=NOTSET, tag=None): 
    73                 """ add an event to the event queue, may upgrade or downgrade the  
    74                 severity, depending on the currently active options.""" 
    75                  
    76                 event = Event(text, self._get_severity(severity, tag), tag) 
    77                 self.events.append(event) 
    78                  
    79         def has_severity(self, severity): 
    80                 """ used to get a count of the number of items of equal or greater  
    81                 severity with CRITICAL being the largest, and NOTSET being the lowest. 
    82                 Useful to check and see if any actual errors occured, in contrast to  
    83                 informational events. 
    84                 """ 
    85                 count = 0 
    86                 for event in self.events: 
    87                         if event.severity >= severity: 
    88                                 count += 1 
    89 #               self.events.append(Event("examined %d events and found %d events of %s or greater" % (len(self.events), count, logging.getLevelName(severity)))) 
    90                 return count 
    91                          
    92 class Event: 
    93         """Structure to trace an indivdual event.""" 
    94         text = None 
    95         tag = None 
    96         severity = NOTSET 
    97         #todo add a datetime 
    98          
    99         def __init__(self, text, severity=NOTSET, tag=None): 
    100                 self.text = text 
    101                 self.severity = severity 
    102                 self.tag = tag 
    103                  
    104          
     97    Nothing fancy here just a plain old object. 
     98     
     99    Todo:: it would be nice to timestamp these  
     100    """ 
     101     
     102    text = None 
     103    tag = None 
     104    severity = NOTSET 
     105     
     106    def __init__(self, text, severity=NOTSET, tag=None): 
     107        self.text = text 
     108        self.severity = severity 
     109        self.tag = tag 
     110         
     111     
  • trunk/rp/common/python/infocard/infocardlib.py

    r1245 r1264  
    11#  Copyright (c) 2007, 2008 Novell, Inc. 
    22#  All Rights Reserved. 
    3 # 
     3 
    44#  This library is free software; you can redistribute it and/or 
    55#  modify it under the terms of the GNU Lesser General Public License as 
     
    8383BEARER_TOKEN = 'urn:oasis:names:tc:SAML:1.0:cm:bearer' 
    8484HOLDER_OF_KEY_TOKEN = 'urn:oasis:names:tc:SAML:1.0:cm:holder-of-key' 
     85SENDER_VOUCHES_TOKEN = "urn:oasis:names:tc:SAML:1.0:cm:sender-vouches" 
    8586 
    8687""" public identifiers for meta data about the token 
     
    113114 
    114115class InfoCardProcessor: 
    115  
     116    """Base object for consumers who wish to build a python based RP for  
     117    processing information cards and dealing with cardspace. 
     118     
     119    This object may be created and configured once then used to evaluate many  
     120    security tokens 
     121         
     122    """ 
     123     
    116124    def __init__(self): 
    117125        self.options = {} 
    118126 
    119127    def setDecode(self, privateKey, passPhrase = None, isFile=False, isCert = True): 
     128        """Setup the cert/private key used to decrypt tokens.  In many cases this  
     129        will be the servers ssl cert. 
     130         
     131        always returns None 
     132        """ 
    120133        if isFile: 
    121134            fp = open(privateKey, 'rb') 
     
    130143 
    131144    def setClaims(self, required, optional = None, multivalued = None): 
     145        """Helper function to simplify setting of the optional and required 
     146        claims, as well as which claims may be multivalued. 
     147         
     148        Failure to tell the processor which claims are expected may result in 
     149        errors being reported.   The process helps verify that all required claims 
     150        were received and that no additional claims were sent. 
     151         
     152        Todo: insert empty claim handling 
     153        Todo: insert custom claim transformations 
     154         
     155        always returns None 
     156        """ 
    132157        self.options[OPTION_required_claims] = required 
    133158        self.options[OPTION_optional_claims] = optional 
     
    135160 
    136161    def setOptions(self, options): 
     162        """Set options for processing the security token 
     163         
     164        The most common options relate to the overriding of event severity, please 
     165        see evemt.py for details of option name and values. 
     166        """ 
    137167        for name in options.keys(): 
    138168            self.options[name] = options[name] 
     
    145175 
    146176    def processToken(self, xmlToken = None): 
    147         """Parse the token""" 
     177        """Parse the token using prevously configured keys, claims and options. 
     178         
     179        returns a SecToken objec, the returned secToken may or may not be valid, 
     180        it is up to the caller to check secToken.isValid() 
     181        """ 
    148182 
    149183        secToken = SecToken(self.options) 
     
    155189 
    156190class SecToken: 
    157     """ Class for the parsing and holding of security token data""" 
     191    """ Class for the parsing and holding of security token data 
     192         
     193        Instantiate, configure, process, check validity 
     194        Typiclly not directly instanciated, instead InfoCardProcessor is used to  
     195        hold the common configuration and as a factory for creating SecTokens 
     196         
     197        Todo:  currently only supports SAML 1.0/1.1 tokens, that should be abstracted out 
     198        to allow for many token types. 
     199         
     200    """ 
    158201 
    159202    def __init__(self, options=None): 
     
    177220             
    178221    def processToken(self, xmlToken, options): 
    179         """ process the token, until processed no data is present 
     222        """Process the token, until processed no data is present 
     223         
     224        returns True if valid, False if invalid.   Events are logged to  
     225        self.eventLog detialing failure reasons. 
    180226        """ 
    181227         
     
    185231                self.eventLog.add_event("Token not supplied or empty",  
    186232                    event.FATAL, 'empty-token') 
    187             objenc = xmlseclibs.XMLSecEnc(self.eventLog) 
    188             self.cryptedDom = minidom.parseString(self.crypted) 
    189             encData = objenc.locateEncryptedData(self.cryptedDom) 
    190             if not encData: 
    191                 self.eventLog.add_event("Cannot locate encrypted"\ 
    192                     " data in security token, please ensure all requests"\ 
    193                     " are sent over SSL.", event.ERROR, 'parse-locate-encrypted') 
    194                 self.decrypted = self.cryptedDom 
    195233            else: 
    196                 objenc.setNode(encData) 
    197                 objenc.type = encData.getAttribute("Type") 
    198  
    199                 key = None 
    200                 objKey = objenc.locateKey() 
    201                 if (objKey): 
    202                     self.objKeyInfo = objenc.locateKeyInfo(objKey) 
    203                     if (self.objKeyInfo): 
    204                         if (self.objKeyInfo.isEncrypted): 
    205                             objencKey = self.objKeyInfo.encryptedCtx 
    206                             self.objKeyInfo.loadKey( 
    207                                 self._option(options, OPTION_CryptoKey),  
    208                                 self._option(options, OPTION_CryptoKeyPass), 
    209                                 self._option(options, OPTION_CryptoKeyIsFile),  
    210                                 self._option(options, OPTION_CryptoKeyIsCert)) 
    211                             key = objencKey.decryptKey(self.objKeyInfo) 
    212  
    213                 if (not objKey) or (not key): 
    214                     self.eventLog.add_event("Error loading key to handle"\ 
    215                     "Decryption", event.FATAL, 'parse-keyload') 
     234                objenc = xmlseclibs.XMLSecEnc(self.eventLog) 
     235                self.cryptedDom = minidom.parseString(self.crypted) 
     236                encData = objenc.locateEncryptedData(self.cryptedDom) 
     237                if not encData: 
     238                    self.eventLog.add_event("Cannot locate encrypted"\ 
     239                        " data in security token, please ensure all requests"\ 
     240                        " are sent over SSL.", event.ERROR, 'parse-locate-encrypted') 
     241                    self.decrypted = self.cryptedDom 
    216242                else: 
    217                     objKey.loadKey(key) 
    218                     decrypt = objenc.decryptNode(objKey, False) 
    219                     if decrypt: 
    220                         # we have the saml token so load er up 
    221                         self.decrypted = minidom.parseString(decrypt) 
     243                    objenc.setNode(encData) 
     244                    objenc.type = encData.getAttribute("Type") 
     245     
     246                    key = None 
     247                    objKey = objenc.locateKey() 
     248                    if (objKey): 
     249                        self.objKeyInfo = objenc.locateKeyInfo(objKey) 
     250                        if (self.objKeyInfo): 
     251                            if (self.objKeyInfo.isEncrypted): 
     252                                objencKey = self.objKeyInfo.encryptedCtx 
     253                                self.objKeyInfo.loadKey( 
     254                                    self._option(options, OPTION_CryptoKey),  
     255                                    self._option(options, OPTION_CryptoKeyPass), 
     256                                    self._option(options, OPTION_CryptoKeyIsFile),  
     257                                    self._option(options, OPTION_CryptoKeyIsCert)) 
     258                                key = objencKey.decryptKey(self.objKeyInfo) 
     259                            else: 
     260                                self.eventLog.add_event("ObjectKeyInfo is not encrypted", 
     261                                    event.INFO, 'parse-object-key-info') 
     262     
     263                    if (not objKey) or (not key): 
     264                        self.eventLog.add_event("Error loading key to handle"\ 
     265                        "Decryption", event.FATAL, 'parse-keyload') 
    222266                    else: 
    223                         self.eventLog.add_event('Unable to decrypt token', 
    224                             event.FATAL, 'decrypt') 
     267                        objKey.loadKey(key) 
     268                        decrypt = objenc.decryptNode(objKey, False) 
     269                        if decrypt: 
     270                            # we have the saml token so load er up 
     271                            self.decrypted = minidom.parseString(decrypt) 
     272                        else: 
     273                            self.eventLog.add_event('Unable to decrypt token', 
     274                                event.FATAL, 'decrypt') 
    225275            if not self.decrypted: 
    226276                self.eventLog.add_event('Unable to parse decrypted token to DOM', 
     
    244294            self.eventLog.add_event("Exception encountered while processing Security Token, exception: %s " % (str(errorMsg)),  
    245295                    event.FATAL, 'exception') 
     296        return self.isValid 
    246297 
    247298    def _setupMiscData(self, options): 
    248         """Setup all of the data about the token we can, not some data like the 
     299        """Setup all of the data about the token we can, note that some data like the 
    249300        audience, valid before/after are set in _isValid 
     301         
     302        Todo:: currently saml specific 
     303         
     304         
    250305        """ 
    251306        rootNode = self.decrypted.documentElement 
     
    260315 
    261316    def _isValid(self, options): 
    262         """ Validate the SAML token 
     317        """ Validate the token, including signature validation, validity period, 
     318        osis interop tests, and that the expected claims are present. 
     319         
     320        There are some metadata items cached by this function 
     321         
     322        Todo:: currently saml specific 
    263323        """ 
    264324        data = self.decrypted.documentElement.namespaceURI 
     
    293353            if (self.objKeyInfo): 
    294354                # Handle any additional key processing such as encrypted keys here 
    295 #               self.eventLog.add_event("Potential additional key processing required: %s, %u" \ 
     355#                self.eventLog.add_event("Potential additional key processing required: %s, %u" \ 
    296356#                   % (self.objKeyInfo.type, objKey.isEncrypted), 
    297357#                   event.INFO) 
    298358                pass 
     359#            else: 
     360#             self.eventLog.add_event("No Key Info found for %u" \ 
     361#                   % (objKey.isEncrypted),  event.INFO) 
    299362            retVal = objXMLSecDSig.verify(objKey) 
    300363            if not retVal: 
     
    337400 
    338401        if not self.metadata.has_key(META_Audience):  
    339             self.eventLog.add_event("Security Token does not contain "\ 
    340                 "an Audience restriction.", event.ERROR, 'validate-audience-present') 
     402            self.eventLog.add_event("Your IdP did not include an Audience "\ 
     403                "restriction in the Security Token.", event.ERROR, 'validate-audience-present') 
    341404 
    342405        query = '/mysaml:Assertion/mysaml:AttributeStatement/mysaml:Subject/mysaml:SubjectConfirmation/mysaml:ConfirmationMethod' 
     
    345408            self.metadata[META_SubjectConfirmation] = node.firstChild.data 
    346409            if (not self._doesMatchFuzzy(BEARER_TOKEN, self.metadata[META_SubjectConfirmation], None, None) and 
    347                not self._doesMatchFuzzy(HOLDER_OF_KEY_TOKEN, self.metadata[META_SubjectConfirmation], None, None)): 
    348                     self.eventLog.add_event("Passed Configuration data \"%s\" doesn't match "\ 
     410               not self._doesMatchFuzzy(HOLDER_OF_KEY_TOKEN, self.metadata[META_SubjectConfirmation], None, None) 
     411               and not  self._doesMatchFuzzy(SENDER_VOUCHES_TOKEN, self.metadata[META_SubjectConfirmation], None, None)): 
     412                    self.eventLog.add_event("Passed configuration data \"%s\" doesn't match "\ 
    349413                    "expected configuration data\"%s\"" %  
    350                     (self.metadata[META_SubjectConfirmation], 'holder-of-key or bearer'), 
     414                    (self.metadata[META_SubjectConfirmation], 'holder-of-key or bearer or sender vouches'), 
    351415                    event.ERROR, 'mismatched-confirmation')  
    352416            break 
     
    390454 
    391455    def _doesMatchFuzzy(self, expected, got, clearTag, eventTag): 
     456        """Lienient matching for items which may be allowed to be missing 
     457         
     458        returns true if item is not required or matches.  returns False and logs 
     459        the event on failure. 
     460        """ 
    392461        if not got or not expected or expected == got or re.match(expected, got): 
    393462            return True 
     
    399468            return False; 
    400469 
    401     #TODO handle namespaces, claim name mapping, multiple values! 
    402470    def _setupAssertions(self, options): 
     471        """Build a cache of assertion names/values 
     472         
     473        Todo:: handle namespaces, claim name mapping, multiple values, empty  
     474        value handling and value attributes! 
     475         
     476        at compleation self.assertions is setup.  Assertions should be accessed 
     477        by calling getAssertionValues, not by accessing self.assertions. 
     478        """ 
    403479        if not self.assertions or len(self.assertions) == 0: 
    404480            mvClaims = [] 
     
    446522    def getAssertionValues(self, identifier=None): 
    447523        """Allows retrivial of any claim or assertion associated with the security token 
     524         
     525        Todo::  visit and finish this function 
    448526        Returns either the data or None 
    449         visit and finish this function! 
    450527        """ 
    451528        #is the identifier a URI or just a short name? 
     
    460537        except Exception: 
    461538            claim = identifier 
    462  
     539             
    463540#       self.eventLog.add_event("Looking for ns: \"%s\"   claim: \"%s\" " \ 
    464541#           % (ns, claim),  
     
    483560            return claims 
    484561 
    485  
    486562    def getMetaDataValues(self, identifier=None): 
    487563        """Allows retrivial of any meta data associated with the security token 
     564         
     565        Note: currently all meta data is single valued! 
    488566        Pass a specific string for the identifier and receive either a  
    489567        string or None 
    490568        If the identifier is None then a dictionary of all meta data is returned 
    491         Currently all meta data is single valued! 
    492569        """ 
    493570        if identifier: 
     
    500577 
    501578    def _getCardKeyHash(self): 
     579        """Internal routine to generate a generic cardkey hash,  the result is  
     580        stored in self.metadata[META_CardKeyHash] 
     581        """ 
    502582        try: 
    503583            return self.metadata[META_CardKeyHash] 
     
    517597            return self.metadata[META_CardKeyHash] 
    518598 
    519 # TODO: need work on ISO date checking - skip for now 
    520599def checkDateConditions(start=None, end=None, difference=300): 
     600    """Validate a datetime is in range.    
     601     
     602    difference in seconds allows for clock skew. 
     603     
     604    Todo:: need work on ISO date checking - skip for now 
     605     
     606    returns True if time is in range False if not in range 
     607    """ 
    521608    currentTime = datetime.datetime.now() 
    522609    if (start): 
    523610        startTime = datetime.datetime.fromtimestamp(xml.utils.iso8601.parse(start)) 
    524         # Allow for a 5 minute difference in Time 
     611        # Allow for a difference in clock syncronization,   
    525612        d = datetime.timedelta(minutes=difference) 
    526613        if ((not startTime) or ((startTime - d) > currentTime)): 
  • trunk/rp/common/python/infocard/xmlseclibs.py

    r1213 r1264  
    1 #  Copyright (c) 2007 Novell, Inc. 
     1#  Copyright (c) 2007,2008 Novell, Inc. 
    22#  All Rights Reserved. 
    3 # 
     3 
    44#  This library is free software; you can redistribute it and/or 
    55#  modify it under the terms of the GNU Lesser General Public License as 
     
    7474 
    7575def report_event(eventLog, text, severity = event.NOTSET, tag = None): 
     76    """ module level function for either remembering the event or throwing an 
     77    exception if eventlonging is not configured. 
     78    """ 
    7679    if eventLog: 
    7780        eventLog.add_event(text, severity, tag) 
     
    8184 
    8285class XMLSecurityKey: 
     86    """Class for encrypting/decrypting data, and remembering information about . 
     87    the methods and keys for doing so. 
     88     
     89    Hides from the caller the details of which library (openssl or mcrypt) is  
     90    used for a particular cipher. 
     91     
     92    """ 
    8393    TRIPLEDES_CBC = 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc' 
    8494    AES128_CBC = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc' 
     
    173183 
    174184    def loadKey(self, key, passPhrase = None, isFile=False, isCert = True): 
     185        """ setup key information in object 
     186         
     187        This function fails if key can't be loaded succesfully 
     188        Returns None on success and failure, reports errors to the configured event log. 
     189        """ 
    175190        if isFile: 
    176191            fp = open(key, 'rb') 
     
    218233        return None 
    219234 
    220     #TODO: Convert encryptMcrypt 
    221235    def encryptMcrypt(self, data): 
     236        """Encrypted using the mcrypt library 
     237         
     238        This function should not be called directly, rather call encryptData and 
     239        let that function switch based on the configured library. 
     240         
     241        Todo:: 
     242        Not currently called, appears to be incomplete, possibly containing some 
     243        phpisms 
     244        """ 
    222245        td = mcrypt_module_open(self.cryptParams['cipher'], '', self.cryptParams['mode'], '') 
    223         self.iv = mcrypt_create_iv (mcrypt_enc_get_iv_size(td), MCRYPT_RAND) 
     246        self.iv = mcrypt_create_iv(mcrypt_enc_get_iv_size(td), MCRYPT_RAND) 
    224247        mcrypt_generic_init(td, self.key, self.iv) 
    225248        encrypted_data = self.iv.mcrypt_generic(td, data) 
     
    229252 
    230253    def decryptMcrypt(self, data): 
     254        """Decrypt using the mcrypt library  
     255         
     256        This function should not be called directly, rather call decryptData and 
     257        let that function switch based on the configured library. 
     258         
     259        Todo:: currently only supports MCRYPT_MODE_CBC  
     260         
     261        returns None on failure, decrypted data on success.   No events logged  
     262        """ 
    231263        dCipher = self.cryptParams['cipher'] 
    232264        iv_length = dCipher.block_size