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

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@6067
Revision 6067, 12.3 KB checked in by pjkersha, 11 years ago (diff)

Re-issue 1.3.3 release:

  • uses SQLAlchemy test user database for authz_lite integration tests
  • important fix for ndg.security.common.saml_utils.bindings.AttributeQuerySOAPBinding: set 'nameFormat' attribute not 'format' for attributes in AttributeQuery?.
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        try:
206            nEntries = int([r[0] for r in result][0])
207           
208        except (ValueError, TypeError), e:
209            raise AuthNInterfaceRetrieveError("Expecting integer count result "
210                                              "from login SQL query: %s" %
211                                              traceback.format_exc())
212        if nEntries < 1:
213            raise AuthNInterfaceInvalidCredentials("Logon query %r: invalid "
214                                                   "password for user %r" % 
215                                                   (query, username))
216        elif nEntries > 1:
217            raise AuthNInterfaceInvalidCredentials("Logon: multiple entries "
218                                                   "returned for query %r" % 
219                                                   query)
220           
221        log.debug('Logon succeeded for user %r' % username)
222
223    def logout(self):
224        """No special functionality is required for logout"""
225       
226    def username2UserIdentifiers(self, environ, username):
227        """Map the login username to an identifier which will become the
228        unique path suffix to the user's OpenID identifier.  The
229        OpenIDProviderMiddleware takes self.urls['id_url'] and adds it to this
230        identifier:
231       
232            identifier = self._authN.username2UserIdentifiers(environ,
233                                                              username)[0]
234            identifierKw = dict(userIdentifier=identifier)
235            identityURL = Template(self.urls['url_id'].substitute(identifierKw)
236       
237        @type environ: dict
238        @param environ: standard WSGI environ parameter
239
240        @type username: basestring
241        @param username: user identifier
242       
243        @rtype: tuple
244        @return: identifiers to be used to make OpenID user identity URLs.
245       
246        @raise AuthNInterfaceRetrieveError: error with retrieval of information
247        to identifier e.g. error with database look-up.
248        @raise AuthNInterfaceConfigError: missing database engine plugin for
249        SQLAlchemy
250        """
251
252        try:
253            dbEngine = create_engine(self.connectionString)
254        except ImportError, e:
255            raise AuthNInterfaceConfigError("Missing database engine for "
256                                            "SQLAlchemy: %s" % e)
257        connection = dbEngine.connect()
258       
259        try:
260            queryInputs = {
261                SQLAlchemyAuthnInterface.USERNAME_SQLQUERY_KEYNAME: username,
262            }
263            queryTmpl = Template(self.username2UserIdentifierSqlQuery)
264            query = queryTmpl.substitute(queryInputs)
265           
266        except KeyError, e:
267            raise AuthNInterfaceConfigError("Invalid key %r for username to "
268                                            "user identifier SQL query string. "
269                                            " The valid key is %r" % (e,
270                            SQLAlchemyAuthnInterface.USERNAME_SQLQUERY_KEYNAME))
271       
272        try:
273            result = connection.execute(query)
274
275        except (exc.ProgrammingError, exc.OperationalError):
276            raise AuthNInterfaceRetrieveError("Error with SQL query: %s" %
277                                              traceback.format_exc())
278        finally:
279            connection.close()
280           
281        userIdentifiers = tuple([i[0] for i in result.fetchall()])     
282        if len(userIdentifiers) == 0:
283            raise AuthNInterfaceInvalidCredentials('No entries for "%s" user' % 
284                                                   username)
285         
286        log.debug('username %r maps to OpenID identifiers: %r', username,
287                  userIdentifiers)
288       
289        return userIdentifiers
290
291
292    def __getstate__(self):
293        '''Enable pickling for use with beaker.session'''
294        _dict = {}
295        for attrName in SQLAlchemyAuthnInterface.__slots__:
296            # Ugly hack to allow for derived classes setting private member
297            # variables
298            if attrName.startswith('__'):
299                attrName = "_SQLAlchemyAuthnInterface" + attrName
300               
301            _dict[attrName] = getattr(self, attrName)
302           
303        return _dict
304           
305    def __setstate__(self, attrDict):
306        '''Enable pickling for use with beaker.session'''
307        for attr, val in attrDict.items():
308            setattr(self, attr, val)           
Note: See TracBrowser for help on using the repository browser.