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

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg-security/TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/wsgi/openid/provider/axinterface/sqlalchemy_ax.py@7077
Revision 7077, 10.0 KB checked in by pjkersha, 11 years ago (diff)
  • Property svn:keywords set to Id
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, AXInterfaceRetrieveError, 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    USERNAME_SESSION_KEYNAME = OpenIDProviderMiddleware.USERNAME_SESSION_KEYNAME
29                       
30    CONNECTION_STRING_OPTNAME = 'connectionString'
31    SQLQUERY_OPTNAME = 'sqlQuery'
32    ATTRIBUTE_NAMES_OPTNAME = "attributeNames"
33    SQLQUERY_USERID_KEYNAME = 'username'
34   
35    ATTR_NAMES = (
36        CONNECTION_STRING_OPTNAME,
37        SQLQUERY_OPTNAME,
38        ATTRIBUTE_NAMES_OPTNAME,
39    )
40    __slots__ = tuple(["__%s" % name for name in ATTR_NAMES])
41    del name
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       
57        self.setProperties(**properties)
58
59    def _getConnectionString(self):
60        return self.__connectionString
61
62    def _setConnectionString(self, value):
63        if not isinstance(value, basestring):
64            raise TypeError('Expecting string type for "%s" '
65                            'attribute; got %r' % 
66                            (SQLAlchemyAXInterface.CONNECTION_STRING_OPTNAME,
67                             type(value)))
68        self.__connectionString = value
69
70    connectionString = property(fget=_getConnectionString, 
71                                fset=_setConnectionString, 
72                                doc="Database connection string")
73
74    def _getSqlQuery(self):
75        return self.__sqlQuery
76
77    def _setSqlQuery(self, value):
78        if not isinstance(value, basestring):
79            raise TypeError('Expecting string type for "sqlQuery" '
80                            'attribute; got %r' % type(value))
81        self.__sqlQuery = value
82
83    sqlQuery = property(fget=_getSqlQuery, 
84                        fset=_setSqlQuery, 
85                        doc="SQL Query for authentication request")
86
87    def _getAttributeNames(self):
88        return self.__attributeNames
89
90    def _setAttributeNames(self, value):
91        """@param value: if a string, it will be parsed into a list delimiting
92        elements by whitespace
93        @type value: basestring/tuple or list
94        """
95        if isinstance(value, (list, tuple)):
96            self.__attributeNames = list(value)
97           
98        elif isinstance(value, basestring):
99            self.__attributeNames = value.split() 
100        else:
101            raise TypeError('Expecting string, list or tuple type for '
102                            '"attributeNames"; got %r' % type(value))
103       
104    attributeNames = property(fget=_getAttributeNames, 
105                              fset=_setAttributeNames, 
106                              doc="list of attribute names supported.  The "
107                                  "order of the names is important and "
108                                  "determines the order in which they will be "
109                                  "assigned to values from the SQL query "
110                                  "result")
111
112    def setProperties(self, **properties):
113        """Set object attributes by keyword argument to this method.  Keywords
114        are restricted by the entries in __slots__
115        """
116        for name, val in properties.items():
117            setattr(self, name, val)
118   
119    def __call__(self, ax_req, ax_resp, authnInterface, authnCtx):
120        """Add the attributes to the ax_resp object requested in the ax_req
121        object.  If it is not possible to return them, raise
122        MissingRequiredAttrs error
123       
124        @type ax_req: openid.extensions.ax.FetchRequest
125        @param ax_req: attribute exchange request object.  To find out what
126        attributes the Relying Party has requested for example, call
127        ax_req.getRequiredAttrs()
128        @type ax_resp: openid.extensions.ax.FetchResponse
129        @param ax_resp: attribute exchange response object.  This method should
130        update the settings in this object.  Use addValue and setValues methods
131        @type authnInterface: AbstractAuthNInterface
132        @param authnInterface: custom authentication interface set at login. 
133        See ndg.security.server.openid.provider.AbstractAuthNInterface for more
134        information
135        @type authnCtx: dict like
136        @param authnCtx: session containing authentication context information
137        such as username and OpenID user identifier URI snippet
138        """
139        log.debug('SQLAlchemyAXInterface.__call__  ...')
140       
141        username = authnCtx.get(SQLAlchemyAXInterface.USERNAME_SESSION_KEYNAME)
142        if username is None:
143            raise AXInterfaceConfigError("No username set in session context")
144       
145        requiredAttributeURIs = ax_req.getRequiredAttrs()
146       
147        if self.attributeNames is None:
148            raise AXInterfaceConfigError('No "attributeNames" setting has '
149                                         'been made')
150
151        missingAttributeURIs = [
152            requiredAttributeURI
153            for requiredAttributeURI in requiredAttributeURIs
154            if requiredAttributeURI not in self.attributeNames
155        ]
156        if len(missingAttributeURIs) > 0:
157            raise MissingRequiredAttrs("OpenID Provider does not support "
158                                       "release of these attributes required "
159                                       "by the Relying Party: %s" %
160                                       ', '.join(missingAttributeURIs))
161
162        # Query for available attributes
163        userAttributeMap = self._attributeQuery(username)
164       
165        # Add the requested attribute if available
166        for requestedAttributeURI in ax_req.requested_attributes.keys():
167            if requestedAttributeURI in self.attributeNames:
168                log.info("Adding requested AX parameter %s=%s ...", 
169                         requestedAttributeURI,
170                         userAttributeMap[requestedAttributeURI])
171               
172                ax_resp.addValue(requestedAttributeURI,
173                                 userAttributeMap[requestedAttributeURI])
174            else:
175                log.info("Skipping Relying Party requested AX parameter %s: "
176                         "this parameter is not available", 
177                         requestedAttributeURI)
178
179    def _attributeQuery(self, username):
180        '''Query the database for attributes and map these to the attribute
181        names given in the configuration.  Overload as required to ensure a
182        correct mapping between the SQL query results and the attribute names
183        they refer to
184        '''           
185        if self.connectionString is None:
186            raise AXInterfaceConfigError('No "connectionString" setting has '
187                                         'been made')
188        dbEngine = create_engine(self.connectionString)
189       
190        try:
191            queryInputs = {
192                SQLAlchemyAXInterface.SQLQUERY_USERID_KEYNAME: username
193            }
194            query = Template(self.sqlQuery).substitute(queryInputs)
195           
196        except KeyError, e:
197            raise AXInterfaceConfigError("Invalid key %r for attribute query "
198                                         "string.  The valid key is %r" % (e, 
199                                SQLAlchemyAXInterface.SQLQUERY_USERID_KEYNAME))
200           
201        connection = dbEngine.connect()
202           
203        try:
204            result = connection.execute(query)
205
206        except (exc.ProgrammingError, exc.OperationalError):
207            raise AXInterfaceRetrieveError("SQL error: %s" %
208                                           traceback.format_exc())
209        finally:
210            connection.close()
211
212        try:
213            attributeValues = result.fetchall()[0]
214        except IndexError:
215            raise AXInterfaceRetrieveError("No attributes returned for "
216                                           "query=\"%s\"" % query)
217       
218        if len(self.attributeNames) != len(attributeValues):
219            raise AXInterfaceConfigError("Attribute query results %r, don't "
220                                         "match the attribute names specified "
221                                         "in the configuration file: %r" %
222                                         (attributeValues, self.attributeNames))
223           
224        attributes = dict(zip(self.attributeNames, attributeValues))
225                         
226        log.debug("Retrieved user AX attributes %r" % attributes)
227       
228        return attributes
229
230    def __getstate__(self):
231        '''Enable pickling for use with beaker.session'''
232        _dict = {}
233        for attrName in SQLALchemyAXInterface.__slots__:
234            # Ugly hack to allow for derived classes setting private member
235            # variables
236            if attrName.startswith('__'):
237                attrName = "_SQLALchemyAXInterface" + attrName
238               
239            _dict[attrName] = getattr(self, attrName)
240           
241        return _dict       
242    def __setstate__(self, attrDict):
243        '''Enable pickling for use with beaker.session'''
244        self.setProperties(**attrDict)
Note: See TracBrowser for help on using the repository browser.