source: TI12-security/trunk/python/ndg_security_server/ndg/security/server/wsgi/openid/provider/authninterface/sqlalchemy_authn.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/authninterface/sqlalchemy_authn.py@5870
Revision 5870, 10.2 KB checked in by pjkersha, 11 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"""
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    CONNECTION_STRING_OPTNAME = 'connectionString'
38    LOGON_SQLQUERY_OPTNAME = 'logonSqlQuery'
39    USERNAME2USERIDENTIFIER_SQLQUERY_OPTNAME = 'username2UserIdentifierSqlQuery'
40    IS_MD5_ENCODED_PWD = 'isMD5EncodedPwd'
41   
42    __slots__ = (
43        CONNECTION_STRING_OPTNAME,
44        LOGON_SQLQUERY_OPTNAME,
45        USERNAME2USERIDENTIFIER_SQLQUERY_OPTNAME,
46        IS_MD5_ENCODED_PWD
47    )
48   
49    def __init__(self, **prop):
50        '''Instantiate object taking in settings from the input
51        properties'''
52        log.debug('Initialising SQLAlchemyAuthnInterface instance ...')
53       
54        self.__connectionString = None
55        self.__logonSqlQuery = None
56        self.__username2UserIdentifierSqlQuery = None
57        self.__isMD5EncodedPwd = False
58       
59        try:
60            self.connectionString = prop[
61                            SQLAlchemyAuthnInterface.CONNECTION_STRING_OPTNAME]
62           
63            self.logonSqlQuery = prop[
64                            SQLAlchemyAuthnInterface.LOGON_SQLQUERY_OPTNAME]
65                     
66            self.username2UserIdentifierSqlQuery = prop[
67            SQLAlchemyAuthnInterface.USERNAME2USERIDENTIFIER_SQLQUERY_OPTNAME]
68 
69            self.isMD5EncodedPwd = prop[
70                            SQLAlchemyAuthnInterface.IS_MD5_ENCODED_PWD]   
71        except KeyError, e:
72            raise AuthNInterfaceConfigError("Initialisation from keywords: %s"%
73                                            e)
74
75    def _getConnectionString(self):
76        return self.__connectionString
77
78    def _setConnectionString(self, value):
79        if not isinstance(value, basestring):
80            raise TypeError('Expecting string type for "%s" attribute; got %r'%
81                            (SQLAlchemyAuthnInterface.CONNECTION_STRING_OPTNAME,
82                             type(value)))
83        self.__connectionString = value
84
85    connectionString = property(fget=_getConnectionString, 
86                                fset=_setConnectionString, 
87                                doc="Database connection string")
88
89    def _getLogonSqlQuery(self):
90        return self.__logonSqlQuery
91
92    def _setLogonSqlQuery(self, value):
93        if not isinstance(value, basestring):
94            raise TypeError('Expecting string type for "%s" '
95                            'attribute; got %r' % 
96                            (SQLAlchemyAuthnInterface.LOGON_SQLQUERY_OPTNAME,
97                             type(value)))
98        self.__logonSqlQuery = value
99
100    logonSqlQuery = property(fget=_getLogonSqlQuery, 
101                        fset=_setLogonSqlQuery, 
102                        doc="SQL Query for authentication request")
103
104    def _getUsername2UserIdentifierSqlQuery(self):
105        return self.__username2UserIdentifierSqlQuery
106
107    def _setUsername2UserIdentifierSqlQuery(self, value):
108        if not isinstance(value, basestring):
109            raise TypeError('Expecting string type for "%s" attribute; got %r'%
110                            (SQLAlchemyAuthnInterface.
111                             USERNAME2USERIDENTIFIER_SQLQUERY_OPTNAME,
112                             type(value)))
113        self.__username2UserIdentifierSqlQuery = value
114
115    username2UserIdentifierSqlQuery = property(
116                                    fget=_getUsername2UserIdentifierSqlQuery, 
117                                    fset=_setUsername2UserIdentifierSqlQuery, 
118                                    doc="SQL Query for OpenID user identifier "
119                                        "look-up")
120   
121    def _getIsMD5EncodedPwd(self):
122        return self.__isMD5EncodedPwd
123
124    def _setIsMD5EncodedPwd(self, value):
125        if isinstance(value, bool):
126            self.__isMD5EncodedPwd = value
127        elif isinstance(value, basestring):
128            self.__isMD5EncodedPwd = SQLAlchemyAuthnInterface.str2Bool(value)
129        else:
130            raise TypeError('Expecting bool type for "isMD5EncodedPwd" '
131                            'attribute; got %r' % type(value))
132
133    isMD5EncodedPwd = property(fget=_getIsMD5EncodedPwd, 
134                               fset=_setIsMD5EncodedPwd,
135                               doc="Boolean set to True if password is MD5 "
136                                   "encrypted")
137
138    def logon(self, environ, identityURI, username, password):
139        """Interface login method
140       
141        @type environ: dict
142        @param environ: standard WSGI environ parameter
143
144        @type identityURI: basestring
145        @param identityURI: user's identity URL e.g.
146        'https://joebloggs.somewhere.ac.uk/'
147
148        @type username: basestring
149        @param username: username
150       
151        @type password: basestring
152        @param password: corresponding password for username givens
153       
154        @raise AuthNInterfaceInvalidCredentials: invalid username/password
155        @raise AuthNInterfaceUsername2IdentifierMismatch: no OpenID matching
156        the given username
157        @raise AuthNInterfaceConfigError: missing database engine plugin for
158        SQLAlchemy
159        """
160        if self.isMD5EncodedPwd:
161            try:
162                _password = md5(password).hexdigest()
163            except Exception, e:
164                raise AuthNInterfaceConfigError("%s exception raised making a "
165                                                "digest of the input "
166                                                "password: %s" % 
167                                                (type(e), 
168                                                 traceback.format_exc()))
169        else:
170            _password = password
171
172        try:
173            dbEngine = create_engine(self.connectionString)
174        except ImportError, e:
175            raise AuthNInterfaceConfigError("Missing database engine for "
176                                            "SQLAlchemy: %s" % e)
177        connection = dbEngine.connect()
178       
179        try:
180            queryInputs = dict(username=username, password=_password)
181            query = Template(self.logonSqlQuery).substitute(queryInputs)
182            result = connection.execute(query)
183
184        except exc.ProgrammingError:
185            raise AuthNInterfaceRetrieveError("Error with SQL Syntax: %s" %
186                                              traceback.format_exc())
187        finally:
188            connection.close()
189
190        if result.rowcount != 1:
191            raise AuthNInterfaceInvalidCredentials()
192       
193        log.debug('Logon succeeded for user %r' % username)
194
195    def logout(self):
196        """No special functionality is required for logout"""
197       
198    def username2UserIdentifiers(self, environ, username):
199        """Map the login username to an identifier which will become the
200        unique path suffix to the user's OpenID identifier.  The
201        OpenIDProviderMiddleware takes self.urls['id_url'] and adds it to this
202        identifier:
203       
204            identifier = self._authN.username2UserIdentifiers(environ,
205                                                              username)[0]
206            identifierKw = dict(userIdentifier=identifier)
207            identityURL = Template(self.urls['url_id'].substitute(identifierKw)
208       
209        @type environ: dict
210        @param environ: standard WSGI environ parameter
211
212        @type username: basestring
213        @param username: user identifier
214       
215        @rtype: tuple
216        @return: identifiers to be used to make OpenID user identity URLs.
217       
218        @raise AuthNInterfaceRetrieveError: error with retrieval of information
219        to identifier e.g. error with database look-up.
220        @raise AuthNInterfaceConfigError: missing database engine plugin for
221        SQLAlchemy
222        """
223
224        try:
225            dbEngine = create_engine(self.connectionString)
226        except ImportError, e:
227            raise AuthNInterfaceConfigError("Missing database engine for "
228                                            "SQLAlchemy: %s" % e)
229        connection = dbEngine.connect()
230       
231        try:
232            queryInputs = dict(username=username)
233            queryTmpl = Template(self.username2UserIdentifierSqlQuery)
234            query = queryTmpl.substitute(queryInputs)
235            result = connection.execute(query)
236
237        except exc.ProgrammingError:
238            raise AuthNInterfaceRetrieveError("Error with SQL Syntax: %s" %
239                                              traceback.format_exc())
240        finally:
241            connection.close()
242           
243        if result.rowcount == 0:
244            raise AuthNInterfaceInvalidCredentials('No entries for "%s" user' % 
245                                                  username)
246           
247        userIdentifiers = tuple([i[0] for i in result.fetchall()])
248         
249        log.debug('username %r maps to OpenID identifiers: %r', username,
250                  userIdentifiers)
251       
252        return userIdentifiers
253
254    def __getstate__(self):
255        '''Explicit pickling required with __slots__'''
256        return dict([(attrName, getattr(self, attrName)) \
257                     for attrName in SQLAlchemyAuthnInterface.__slots__])
258       
259    def __setstate__(self, attrDict):
260        '''Enable pickling for use with beaker.session'''
261        for attr, val in attrDict.items():
262            setattr(self, attr, val)           
Note: See TracBrowser for help on using the repository browser.