source: TI12-security/trunk/python/ndg_security_server/ndg/security/server/wsgi/openid/provider/authninterface/sqlalchemy_authn.py @ 6064

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg_security_server/ndg/security/server/wsgi/openid/provider/authninterface/sqlalchemy_authn.py@6064
Revision 6064, 11.7 KB checked in by pjkersha, 11 years ago (diff)
  • Fixed slots definitions from global search
  • reran nosetests
Line 
1"""
2SQLAlchemy based Authentication interface for the OpenID Provider
3
4NERC DataGrid Project
5"""
6__author__ = "P J Kershaw"
7__date__ = "20/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__)
14try:
15    from hashlib import md5
16except ImportError:
17    # Allow for < Python 2.5
18    from md5 import md5
19
20import traceback
21from string import Template
22from sqlalchemy import create_engine, exc
23
24from ndg.security.common.utils import str2Bool as _str2Bool
25from ndg.security.server.wsgi.openid.provider.authninterface import (
26    AbstractAuthNInterface, AuthNInterfaceInvalidCredentials, 
27    AuthNInterfaceRetrieveError, AuthNInterfaceConfigError, 
28    AuthNInterfaceUsername2IdentifierMismatch)
29
30
31class SQLAlchemyAuthnInterface(AbstractAuthNInterface):
32    '''Provide a database based Authentication interface to the OpenID Provider
33    making use of the SQLAlchemy database package'''
34   
35    str2Bool = staticmethod(_str2Bool)
36   
37    USERNAME_SQLQUERY_KEYNAME = 'username'
38    PASSWD_SQLQUERY_KEYNAME = 'password'
39    CONNECTION_STRING_OPTNAME = 'connectionString'
40    LOGON_SQLQUERY_OPTNAME = 'logonSqlQuery'
41    USERNAME2USERIDENTIFIER_SQLQUERY_OPTNAME = 'username2UserIdentifierSqlQuery'
42    IS_MD5_ENCODED_PWD = 'isMD5EncodedPwd'
43   
44    ATTR_NAMES = (
45        CONNECTION_STRING_OPTNAME,
46        LOGON_SQLQUERY_OPTNAME,
47        USERNAME2USERIDENTIFIER_SQLQUERY_OPTNAME,
48        IS_MD5_ENCODED_PWD
49    )
50    __slots__ = tuple(["__%s" % name for name in ATTR_NAMES])
51   
52    def __init__(self, **prop):
53        '''Instantiate object taking in settings from the input
54        properties'''
55        log.debug('Initialising SQLAlchemyAuthnInterface instance ...')
56       
57        self.__connectionString = None
58        self.__logonSqlQuery = None
59        self.__username2UserIdentifierSqlQuery = None
60        self.__isMD5EncodedPwd = False
61       
62        try:
63            self.connectionString = prop[
64                            SQLAlchemyAuthnInterface.CONNECTION_STRING_OPTNAME]
65           
66            self.logonSqlQuery = prop[
67                            SQLAlchemyAuthnInterface.LOGON_SQLQUERY_OPTNAME]
68                     
69            self.username2UserIdentifierSqlQuery = prop[
70            SQLAlchemyAuthnInterface.USERNAME2USERIDENTIFIER_SQLQUERY_OPTNAME]
71 
72            self.isMD5EncodedPwd = prop[
73                            SQLAlchemyAuthnInterface.IS_MD5_ENCODED_PWD]   
74        except KeyError, e:
75            raise AuthNInterfaceConfigError("Initialisation from keywords: %s"%
76                                            e)
77
78    def _getConnectionString(self):
79        return self.__connectionString
80
81    def _setConnectionString(self, value):
82        if not isinstance(value, basestring):
83            raise TypeError('Expecting string type for "%s" attribute; got %r'%
84                            (SQLAlchemyAuthnInterface.CONNECTION_STRING_OPTNAME,
85                             type(value)))
86        self.__connectionString = value
87
88    connectionString = property(fget=_getConnectionString, 
89                                fset=_setConnectionString, 
90                                doc="Database connection string")
91
92    def _getLogonSqlQuery(self):
93        return self.__logonSqlQuery
94
95    def _setLogonSqlQuery(self, value):
96        if not isinstance(value, basestring):
97            raise TypeError('Expecting string type for "%s" '
98                            'attribute; got %r' % 
99                            (SQLAlchemyAuthnInterface.LOGON_SQLQUERY_OPTNAME,
100                             type(value)))
101        self.__logonSqlQuery = value
102
103    logonSqlQuery = property(fget=_getLogonSqlQuery, 
104                        fset=_setLogonSqlQuery, 
105                        doc="SQL Query for authentication request")
106
107    def _getUsername2UserIdentifierSqlQuery(self):
108        return self.__username2UserIdentifierSqlQuery
109
110    def _setUsername2UserIdentifierSqlQuery(self, value):
111        if not isinstance(value, basestring):
112            raise TypeError('Expecting string type for "%s" attribute; got %r'%
113                            (SQLAlchemyAuthnInterface.
114                             USERNAME2USERIDENTIFIER_SQLQUERY_OPTNAME,
115                             type(value)))
116        self.__username2UserIdentifierSqlQuery = value
117
118    username2UserIdentifierSqlQuery = property(
119                                    fget=_getUsername2UserIdentifierSqlQuery, 
120                                    fset=_setUsername2UserIdentifierSqlQuery, 
121                                    doc="SQL Query for OpenID user identifier "
122                                        "look-up")
123   
124    def _getIsMD5EncodedPwd(self):
125        return self.__isMD5EncodedPwd
126
127    def _setIsMD5EncodedPwd(self, value):
128        if isinstance(value, bool):
129            self.__isMD5EncodedPwd = value
130        elif isinstance(value, basestring):
131            self.__isMD5EncodedPwd = SQLAlchemyAuthnInterface.str2Bool(value)
132        else:
133            raise TypeError('Expecting bool type for "isMD5EncodedPwd" '
134                            'attribute; got %r' % type(value))
135
136    isMD5EncodedPwd = property(fget=_getIsMD5EncodedPwd, 
137                               fset=_setIsMD5EncodedPwd,
138                               doc="Boolean set to True if password is MD5 "
139                                   "encrypted")
140
141    def logon(self, environ, identityURI, username, password):
142        """Interface login method
143       
144        @type environ: dict
145        @param environ: standard WSGI environ parameter
146
147        @type identityURI: basestring
148        @param identityURI: user's identity URL e.g.
149        'https://joebloggs.somewhere.ac.uk/'
150
151        @type username: basestring
152        @param username: username
153       
154        @type password: basestring
155        @param password: corresponding password for username given
156       
157        @raise AuthNInterfaceInvalidCredentials: invalid username/password
158        @raise AuthNInterfaceUsername2IdentifierMismatch: no OpenID matching
159        the given username
160        @raise AuthNInterfaceConfigError: missing database engine plugin for
161        SQLAlchemy
162        """
163        if self.isMD5EncodedPwd:
164            try:
165                _password = md5(password).hexdigest()
166            except Exception, e:
167                raise AuthNInterfaceConfigError("%s exception raised making a "
168                                                "digest of the input "
169                                                "password: %s" % 
170                                                (type(e), 
171                                                 traceback.format_exc()))
172        else:
173            _password = password
174
175        try:
176            dbEngine = create_engine(self.connectionString)
177        except ImportError, e:
178            raise AuthNInterfaceConfigError("Missing database engine for "
179                                            "SQLAlchemy: %s" % e)
180        connection = dbEngine.connect()
181       
182        try:
183            queryInputs = {
184                SQLAlchemyAuthnInterface.USERNAME_SQLQUERY_KEYNAME: username,
185                SQLAlchemyAuthnInterface.PASSWD_SQLQUERY_KEYNAME: _password
186            }
187            query = Template(self.logonSqlQuery).substitute(queryInputs)
188           
189        except KeyError, e:
190            raise AuthNInterfaceConfigError("Invalid key %r for logon SQL "
191                "query string.  Valid keys are %r and %r" %
192                (e, 
193                 SQLAlchemyAuthnInterface.USERNAME_SQLQUERY_KEYNAME,
194                 SQLAlchemyAuthnInterface.PASSWD_SQLQUERY_KEYNAME))
195           
196        try:
197            result = connection.execute(query)
198
199        except (exc.ProgrammingError, exc.OperationalError):
200            raise AuthNInterfaceRetrieveError("Error with SQL: %s" %
201                                              traceback.format_exc())
202        finally:
203            connection.close()
204           
205        nEntries = int([r[0] for r in result][0])
206        if nEntries != 1:
207            raise AuthNInterfaceInvalidCredentials("Invalid password for user "
208                                                   "%r" % username)
209       
210        log.debug('Logon succeeded for user %r' % username)
211
212    def logout(self):
213        """No special functionality is required for logout"""
214       
215    def username2UserIdentifiers(self, environ, username):
216        """Map the login username to an identifier which will become the
217        unique path suffix to the user's OpenID identifier.  The
218        OpenIDProviderMiddleware takes self.urls['id_url'] and adds it to this
219        identifier:
220       
221            identifier = self._authN.username2UserIdentifiers(environ,
222                                                              username)[0]
223            identifierKw = dict(userIdentifier=identifier)
224            identityURL = Template(self.urls['url_id'].substitute(identifierKw)
225       
226        @type environ: dict
227        @param environ: standard WSGI environ parameter
228
229        @type username: basestring
230        @param username: user identifier
231       
232        @rtype: tuple
233        @return: identifiers to be used to make OpenID user identity URLs.
234       
235        @raise AuthNInterfaceRetrieveError: error with retrieval of information
236        to identifier e.g. error with database look-up.
237        @raise AuthNInterfaceConfigError: missing database engine plugin for
238        SQLAlchemy
239        """
240
241        try:
242            dbEngine = create_engine(self.connectionString)
243        except ImportError, e:
244            raise AuthNInterfaceConfigError("Missing database engine for "
245                                            "SQLAlchemy: %s" % e)
246        connection = dbEngine.connect()
247       
248        try:
249            queryInputs = {
250                SQLAlchemyAuthnInterface.USERNAME_SQLQUERY_KEYNAME: username,
251            }
252            queryTmpl = Template(self.username2UserIdentifierSqlQuery)
253            query = queryTmpl.substitute(queryInputs)
254           
255        except KeyError, e:
256            raise AuthNInterfaceConfigError("Invalid key %r for username to "
257                                            "user identifier SQL query string. "
258                                            " The valid key is %r" % (e,
259                            SQLAlchemyAuthnInterface.USERNAME_SQLQUERY_KEYNAME))
260       
261        try:
262            result = connection.execute(query)
263
264        except (exc.ProgrammingError, exc.OperationalError):
265            raise AuthNInterfaceRetrieveError("Error with SQL query: %s" %
266                                              traceback.format_exc())
267        finally:
268            connection.close()
269           
270        userIdentifiers = tuple([i[0] for i in result.fetchall()])     
271        if len(userIdentifiers) == 0:
272            raise AuthNInterfaceInvalidCredentials('No entries for "%s" user' % 
273                                                   username)
274         
275        log.debug('username %r maps to OpenID identifiers: %r', username,
276                  userIdentifiers)
277       
278        return userIdentifiers
279
280
281    def __getstate__(self):
282        '''Enable pickling for use with beaker.session'''
283        _dict = {}
284        for attrName in SQLAlchemyAuthnInterface.__slots__:
285            # Ugly hack to allow for derived classes setting private member
286            # variables
287            if attrName.startswith('__'):
288                attrName = "_SQLAlchemyAuthnInterface" + attrName
289               
290            _dict[attrName] = getattr(self, attrName)
291           
292        return _dict
293           
294    def __setstate__(self, attrDict):
295        '''Enable pickling for use with beaker.session'''
296        for attr, val in attrDict.items():
297            setattr(self, attr, val)           
Note: See TracBrowser for help on using the repository browser.