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

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.py@5864
Revision 5864, 9.8 KB checked in by pjkersha, 10 years ago (diff)

Added sqlalchemy module for SQLAlchemyAuthnInterface plugin to OpenID Provider.

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