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

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@5870
Revision 5870, 9.7 KB checked in by pjkersha, 10 years ago (diff)
  • added an SQLAlchemy based AX interface for the OpenID Provider
  • Modified the openid_dbinterface egg to enable creation of a unique OpenID identifier based on a given database user table key
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   
35    __slots__ = (
36        CONNECTION_STRING_OPTNAME,
37        SQLQUERY_OPTNAME,
38        ATTRIBUTE_NAMES_OPTNAME
39    )
40   
41    def __init__(self, **properties):
42        '''Instantiate object taking in settings from the input
43        properties
44       
45        @type properties: dict
46        @param properties: keywords corresponding instance attributes - see
47        __slots__ for list of options
48        '''
49        log.debug('Initialising SQLAlchemyAXInterface instance ...')
50       
51        self.__connectionString = None
52        self.__sqlQuery = None
53        self.__attributeNames = None
54       
55        self.setProperties(**properties)
56
57    def _getConnectionString(self):
58        return self.__connectionString
59
60    def _setConnectionString(self, value):
61        if not isinstance(value, basestring):
62            raise TypeError('Expecting string type for "connectionString" '
63                            'attribute; got %r' % type(value))
64        self.__connectionString = value
65
66    connectionString = property(fget=_getConnectionString, 
67                                fset=_setConnectionString, 
68                                doc="Database connection string")
69
70    def _getSqlQuery(self):
71        return self.__sqlQuery
72
73    def _setSqlQuery(self, value):
74        if not isinstance(value, basestring):
75            raise TypeError('Expecting string type for "sqlQuery" '
76                            'attribute; got %r' % type(value))
77        self.__sqlQuery = value
78
79    sqlQuery = property(fget=_getSqlQuery, 
80                        fset=_setSqlQuery, 
81                        doc="SQL Query for authentication request")
82
83    def _getAttributeNames(self):
84        return self.__attributeNames
85
86    def _setAttributeNames(self, value):
87        """@param value: if a string, it will be parsed into a list delimiting
88        elements by whitespace
89        @type value: basestring/tuple or list
90        """
91        if isinstance(value, (list, tuple)):
92            self.__attributeNames = list(value)
93           
94        elif isinstance(value, basestring):
95            self.__attributeNames = value.split() 
96        else:
97            raise TypeError('Expecting string, list or tuple type for '
98                            '"attributeNames"; got %r' % type(value))
99       
100    attributeNames = property(fget=_getAttributeNames, 
101                              fset=_setAttributeNames, 
102                              doc="list of attribute names supported.  The "
103                                  "order of the names is important and "
104                                  "determines the order in which they will be "
105                                  "assigned to values from the SQL query "
106                                  "result")
107
108    def setProperties(self, **properties):
109        for name, val in properties.items():
110            setattr(self, name, val)
111   
112    def __call__(self, ax_req, ax_resp, authnInterface, authnCtx):
113        """Add the attributes to the ax_resp object requested in the ax_req
114        object.  If it is not possible to return them, raise
115        MissingRequiredAttrs error
116       
117        @type ax_req: openid.extensions.ax.FetchRequest
118        @param ax_req: attribute exchange request object.  To find out what
119        attributes the Relying Party has requested for example, call
120        ax_req.getRequiredAttrs()
121        @type ax_resp: openid.extensions.ax.FetchResponse
122        @param ax_resp: attribute exchange response object.  This method should
123        update the settings in this object.  Use addValue and setValues methods
124        @type authnInterface: AbstractAuthNInterface
125        @param authnInterface: custom authentication interface set at login. 
126        See ndg.security.server.openid.provider.AbstractAuthNInterface for more
127        information
128        @type authnCtx: dict like
129        @param authnCtx: session containing authentication context information
130        such as username and OpenID user identifier URI snippet
131        """
132        log.debug('SQLAlchemyAXInterface.__call__  ...')
133       
134        identityURI = authnCtx.get(
135                            SQLAlchemyAXInterface.IDENTITY_URI_SESSION_KEYNAME)
136        if identityURI is None:
137            raise AXInterfaceConfigError("No OpenID user identifier set in "
138                                         "session context")
139       
140        requiredAttributeURIs = ax_req.getRequiredAttrs()
141                                     
142        missingAttributeURIs = [
143            requiredAttributeURI
144            for requiredAttributeURI in requiredAttributeURIs
145            if requiredAttributeURI not in self.attributeNames
146        ]
147        if len(missingAttributeURIs) > 0:
148            raise MissingRequiredAttrs("OpenID Provider does not support "
149                                       "release of these attributes required "
150                                       "by the Relying Party: %s" %
151                                       ', '.join(missingAttributeURIs))
152
153        # Query for available attributes
154        connection = self._makeDbConnection()
155        userAttributeMap = self._attributeQuery(identityURI)
156       
157        # Add the required attributes
158        for requiredAttributeURI in requiredAttributeURIs:
159            log.info("Adding required AX parameter %s=%s ...", 
160                     requiredAttributeURI,
161                     userAttributeMap[requiredAttributeURI])
162           
163            ax_resp.addValue(requiredAttributeURI,
164                             userAttributeMap[requiredAttributeURI])
165           
166        # Append requested attribute if available
167        for requestedAttributeURI in ax_req.requested_attributes.keys():
168            if requestedAttributeURI in self.attributeNames:
169                log.info("Adding requested AX parameter %s=%s ...", 
170                         requestedAttributeURI,
171                         userAttributeMap[requestedAttributeURI])
172               
173                ax_resp.addValue(requestedAttributeURI,
174                                 userAttributeMap[requestedAttributeURI])
175            else:
176                log.info("Skipping Relying Party requested AX parameter %s: "
177                         "this parameter is not available", 
178                         requestedAttributeURI)
179
180    def _makeDbConnection(self):
181        """Create a database connection
182        @rtype: SQLAlchemy database engine
183        @return: database connection object
184        """         
185        try:
186            dbEngine = create_engine(self.connectionString)
187        except ImportError, e:
188            raise AuthNInterfaceConfigError("Missing database engine for "
189                                            "SQLAlchemy: %s" % e)
190        return dbEngine.connect()
191
192    def _attributeQuery(self, username):
193        '''Query the database for attributes and map these to the attribute
194        names given in the configuration.  Overload as required to ensure a
195        correct mapping between the SQL query results and the attribute names
196        they refer to
197        '''
198        try:
199            queryInputs = dict(username=username)
200            query = Template(self.sqlQuery).substitute(queryInputs)
201            result = connection.execute(query)
202
203        except exc.ProgrammingError:
204            raise AuthNInterfaceRetrieveError("Error with SQL Syntax: %s" %
205                                              traceback.format_exc())
206        finally:
207            connection.close()
208
209        if result.rowcount == 0:
210            raise AXInterfaceConfigError("No attribute entry for user [%s] " %
211                                         username)
212        elif result.rowcount > 1:
213            raise AXInterfaceConfigError("Multiple attribute entries for user "
214                                         "[%s] " % username)
215       
216        attributeValues = result.fetchall()[0]
217        if len(self.attributeNames) != len(attributeValues):
218            raise AXInterfaceConfigError("Attribute query results %r, don't "
219                                         "match the attribute names specified "
220                                         "in the configuration file: %r" %
221                                         (attributeValues, self.attributeNames))
222           
223        attributes = dict(zip(self.attributeNames, attributeValues))
224                         
225        log.debug("Retrieved user AX attributes %r" % attributes)
226       
227        return attributes
228   
229    def __getstate__(self):
230        '''Explicit pickling required with __slots__'''
231        return dict([(attrName, getattr(self, attrName)) \
232                     for attrName in SQLAlchemyAXInterface.__slots__])
233       
234    def __setstate__(self, attrDict):
235        '''Enable pickling for use with beaker.session'''
236        self.setProperties(**attrDict)
Note: See TracBrowser for help on using the repository browser.