source: TI12-security/trunk/python/ndg_security_server/ndg/security/server/wsgi/openid/provider/axinterface/sqlalchemy_ax.py @ 5880

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg_security_server/ndg/security/server/wsgi/openid/provider/axinterface/sqlalchemy_ax.py@5880
Revision 5880, 10.8 KB checked in by pjkersha, 10 years ago (diff)

added identity URI template param

Line 
1"""NDG Security OpenID Provider AX Interface for the SQLAlchemy database
2toolkit
3
4NERC DataGrid Project
5"""
6__author__ = "P J Kershaw"
7__date__ = "23/10/09"
8__copyright__ = "(C) 2009 Science and Technology Facilities Council"
9__license__ = "BSD - see LICENSE file in top-level directory"
10__contact__ = "Philip.Kershaw@stfc.ac.uk"
11__revision__ = "$Id$"
12import logging
13log = logging.getLogger(__name__)
14
15import traceback
16from string import Template
17from sqlalchemy import create_engine, exc
18
19from ndg.security.server.wsgi.openid.provider.axinterface import (AXInterface, 
20    AXInterfaceConfigError, MissingRequiredAttrs)
21from ndg.security.server.wsgi.openid.provider import OpenIDProviderMiddleware
22
23
24class SQLAlchemyAXInterface(AXInterface):
25    '''Provide a database based AX interface to the OpenID Provider
26    making use of the SQLAlchemy database package'''
27   
28    IDENTITY_URI_SESSION_KEYNAME = \
29                        OpenIDProviderMiddleware.IDENTITY_URI_SESSION_KEYNAME
30   
31    CONNECTION_STRING_OPTNAME = 'connectionString'
32    SQLQUERY_OPTNAME = 'sqlQuery'
33    ATTRIBUTE_NAMES_OPTNAME = "attributeNames"
34    IDENTITY_URI_TMPL_OPTNAME = 'identityUriTemplate'
35   
36    __slots__ = (
37        CONNECTION_STRING_OPTNAME,
38        SQLQUERY_OPTNAME,
39        ATTRIBUTE_NAMES_OPTNAME,
40        IDENTITY_URI_TMPL_OPTNAME
41    )
42   
43    def __init__(self, **properties):
44        '''Instantiate object taking in settings from the input
45        properties
46       
47        @type properties: dict
48        @param properties: keywords corresponding instance attributes - see
49        __slots__ for list of options
50        '''
51        log.debug('Initialising SQLAlchemyAXInterface instance ...')
52       
53        self.__connectionString = None
54        self.__sqlQuery = None
55        self.__attributeNames = None
56        self.__identityUriTemplate = None
57       
58        self.setProperties(**properties)
59
60    def _getIdentityUriTemplate(self):
61        return self.__identityUriTemplate
62
63    def _setIdentityUriTemplate(self, value):
64        if not isinstance(value, basestring):
65            raise TypeError('Expecting string type for "%s" '
66                            'attribute; got %r' % 
67                            (SQLAlchemyAXInterface.IDENTITY_URI_TMPL_OPTNAME,
68                             type(value)))
69        self.__identityUriTemplate = value
70
71    identityUriTemplate = property(_getIdentityUriTemplate, 
72                                   _setIdentityUriTemplate, 
73                                   doc="Identity URI template string - sets "
74                                       "the common component of user's "
75                                       "identity URI.  It should contain the "
76                                       "${userIdentifier} template "
77                                       "substitution parameter")
78
79    def _getConnectionString(self):
80        return self.__connectionString
81
82    def _setConnectionString(self, value):
83        if not isinstance(value, basestring):
84            raise TypeError('Expecting string type for "%s" '
85                            'attribute; got %r' % 
86                            (SQLAlchemyAXInterface.CONNECTION_STRING_OPTNAME,
87                             type(value)))
88        self.__connectionString = value
89
90    connectionString = property(fget=_getConnectionString, 
91                                fset=_setConnectionString, 
92                                doc="Database connection string")
93
94    def _getSqlQuery(self):
95        return self.__sqlQuery
96
97    def _setSqlQuery(self, value):
98        if not isinstance(value, basestring):
99            raise TypeError('Expecting string type for "sqlQuery" '
100                            'attribute; got %r' % type(value))
101        self.__sqlQuery = value
102
103    sqlQuery = property(fget=_getSqlQuery, 
104                        fset=_setSqlQuery, 
105                        doc="SQL Query for authentication request")
106
107    def _getAttributeNames(self):
108        return self.__attributeNames
109
110    def _setAttributeNames(self, value):
111        """@param value: if a string, it will be parsed into a list delimiting
112        elements by whitespace
113        @type value: basestring/tuple or list
114        """
115        if isinstance(value, (list, tuple)):
116            self.__attributeNames = list(value)
117           
118        elif isinstance(value, basestring):
119            self.__attributeNames = value.split() 
120        else:
121            raise TypeError('Expecting string, list or tuple type for '
122                            '"attributeNames"; got %r' % type(value))
123       
124    attributeNames = property(fget=_getAttributeNames, 
125                              fset=_setAttributeNames, 
126                              doc="list of attribute names supported.  The "
127                                  "order of the names is important and "
128                                  "determines the order in which they will be "
129                                  "assigned to values from the SQL query "
130                                  "result")
131
132    def setProperties(self, **properties):
133        for name, val in properties.items():
134            setattr(self, name, val)
135   
136    def __call__(self, ax_req, ax_resp, authnInterface, authnCtx):
137        """Add the attributes to the ax_resp object requested in the ax_req
138        object.  If it is not possible to return them, raise
139        MissingRequiredAttrs error
140       
141        @type ax_req: openid.extensions.ax.FetchRequest
142        @param ax_req: attribute exchange request object.  To find out what
143        attributes the Relying Party has requested for example, call
144        ax_req.getRequiredAttrs()
145        @type ax_resp: openid.extensions.ax.FetchResponse
146        @param ax_resp: attribute exchange response object.  This method should
147        update the settings in this object.  Use addValue and setValues methods
148        @type authnInterface: AbstractAuthNInterface
149        @param authnInterface: custom authentication interface set at login. 
150        See ndg.security.server.openid.provider.AbstractAuthNInterface for more
151        information
152        @type authnCtx: dict like
153        @param authnCtx: session containing authentication context information
154        such as username and OpenID user identifier URI snippet
155        """
156        log.debug('SQLAlchemyAXInterface.__call__  ...')
157       
158        identityURI = authnCtx.get(
159                            SQLAlchemyAXInterface.IDENTITY_URI_SESSION_KEYNAME)
160        if identityURI is None:
161            raise AXInterfaceConfigError("No OpenID user identifier set in "
162                                         "session context")
163       
164        requiredAttributeURIs = ax_req.getRequiredAttrs()
165                                     
166        missingAttributeURIs = [
167            requiredAttributeURI
168            for requiredAttributeURI in requiredAttributeURIs
169            if requiredAttributeURI not in self.attributeNames
170        ]
171        if len(missingAttributeURIs) > 0:
172            raise MissingRequiredAttrs("OpenID Provider does not support "
173                                       "release of these attributes required "
174                                       "by the Relying Party: %s" %
175                                       ', '.join(missingAttributeURIs))
176
177        # Query for available attributes
178        connection = self._makeDbConnection()
179        userAttributeMap = self._attributeQuery(identityURI)
180       
181        # Add the required attributes
182        for requiredAttributeURI in requiredAttributeURIs:
183            log.info("Adding required AX parameter %s=%s ...", 
184                     requiredAttributeURI,
185                     userAttributeMap[requiredAttributeURI])
186           
187            ax_resp.addValue(requiredAttributeURI,
188                             userAttributeMap[requiredAttributeURI])
189           
190        # Append requested attribute if available
191        for requestedAttributeURI in ax_req.requested_attributes.keys():
192            if requestedAttributeURI in self.attributeNames:
193                log.info("Adding requested AX parameter %s=%s ...", 
194                         requestedAttributeURI,
195                         userAttributeMap[requestedAttributeURI])
196               
197                ax_resp.addValue(requestedAttributeURI,
198                                 userAttributeMap[requestedAttributeURI])
199            else:
200                log.info("Skipping Relying Party requested AX parameter %s: "
201                         "this parameter is not available", 
202                         requestedAttributeURI)
203
204    def _makeDbConnection(self):
205        """Create a database connection
206        @rtype: SQLAlchemy database engine
207        @return: database connection object
208        """         
209        try:
210            dbEngine = create_engine(self.connectionString)
211        except ImportError, e:
212            raise AuthNInterfaceConfigError("Missing database engine for "
213                                            "SQLAlchemy: %s" % e)
214        return dbEngine.connect()
215
216    def _attributeQuery(self, username):
217        '''Query the database for attributes and map these to the attribute
218        names given in the configuration.  Overload as required to ensure a
219        correct mapping between the SQL query results and the attribute names
220        they refer to
221        '''
222        try:
223            queryInputs = dict(username=username)
224            query = Template(self.sqlQuery).substitute(queryInputs)
225            result = connection.execute(query)
226
227        except exc.ProgrammingError:
228            raise AuthNInterfaceRetrieveError("Error with SQL Syntax: %s" %
229                                              traceback.format_exc())
230        finally:
231            connection.close()
232
233        if result.rowcount == 0:
234            raise AXInterfaceConfigError("No attribute entry for user [%s] " %
235                                         username)
236        elif result.rowcount > 1:
237            raise AXInterfaceConfigError("Multiple attribute entries for user "
238                                         "[%s] " % username)
239       
240        attributeValues = result.fetchall()[0]
241        if len(self.attributeNames) != len(attributeValues):
242            raise AXInterfaceConfigError("Attribute query results %r, don't "
243                                         "match the attribute names specified "
244                                         "in the configuration file: %r" %
245                                         (attributeValues, self.attributeNames))
246           
247        attributes = dict(zip(self.attributeNames, attributeValues))
248                         
249        log.debug("Retrieved user AX attributes %r" % attributes)
250       
251        return attributes
252   
253    def __getstate__(self):
254        '''Explicit pickling required with __slots__'''
255        return dict([(attrName, getattr(self, attrName)) \
256                     for attrName in SQLAlchemyAXInterface.__slots__])
257       
258    def __setstate__(self, attrDict):
259        '''Enable pickling for use with beaker.session'''
260        self.setProperties(**attrDict)
Note: See TracBrowser for help on using the repository browser.