diff --git a/src/python/dm/common/service/__init__.py b/src/python/dm/common/service/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/python/dm/common/service/dmController.py b/src/python/dm/common/service/dmController.py new file mode 100755 index 0000000000000000000000000000000000000000..f65d9b27390b3866dec1203ea4aeb4acb8e2f9e7 --- /dev/null +++ b/src/python/dm/common/service/dmController.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python + +# +# Base DM controller class. +# + +####################################################################### + +import cherrypy +import httplib +import json + +from sys import exc_info +from dm.common.utility.loggingManager import LoggingManager +from dm.common.constants import dmStatus +from dm.common.constants import dmHttpHeaders +from dm.common.exceptions.dmException import DmException +from dm.common.exceptions import dmExceptionMap +from dm.common.exceptions.internalError import InternalError + +####################################################################### + +class DmController(object): + """ Base controller class. """ + def __init__(self): + self.logger = LoggingManager.getInstance().getLogger(self.__class__.__name__) + + @classmethod + def getLogger(cls): + logger = LoggingManager.getInstance().getLogger(cls.__name__) + return logger + + @classmethod + def addDmResponseHeaders(cls, status=dmStatus.DM_OK, msg='Success', exceptionType=None): + cherrypy.response.headers[dmHttpHeaders.DM_STATUS_CODE_HTTP_HEADER] = status + cherrypy.response.headers[dmHttpHeaders.DM_STATUS_MESSAGE_HTTP_HEADER] = msg + if exceptionType is not None: + cherrypy.response.headers[dmHttpHeaders.DM_EXCEPTION_TYPE_HTTP_HEADER] = exceptionType + + @classmethod + def addDmSessionRoleHeaders(cls, role): + cherrypy.response.headers[dmHttpHeaders.DM_SESSION_ROLE_HTTP_HEADER] = role + + @classmethod + def addDmExceptionHeaders(cls, ex): + cls.handleException(ex) + + @classmethod + def handleCpException(cls): + cherrypy.response.status = httplib.OK + ex = exc_info()[1] + if ex == None: + ex = InternalError('Internal Webservice Error') + cls.handleException(ex) + + @classmethod + def handleException(cls, ex): + exClass = ex.__class__.__name__.split('.')[-1] + status = None + msg = '%s' % ex + msg = msg.split('\n')[0] + for code in dmExceptionMap.DM_EXCEPTION_MAP.keys(): + exStr = dmExceptionMap.DM_EXCEPTION_MAP.get(code).split('.')[-1] + if exStr == exClass: + status = code + if not status: + status = dmStatus.DM_ERROR + cls.addDmResponseHeaders(status, msg, exClass) + + @classmethod + def formatJsonResponse(cls, response): + cherrypy.response.headers['Content-Type'] = 'application/json' + return '%s' % (response) + + @classmethod + def toJson(cls, o): + return json.dumps(o) + + @classmethod + def fromJson(cls, s): + return json.loads(s) + + @classmethod + def listToJson(cls, dmObjectList): + jsonList = [] + for dmObject in dmObjectList: + jsonList.append(dmObject.getDictRep(keyList='__all__')) + return json.dumps(jsonList) + + @classmethod + def getSessionUser(cls): + return cherrypy.serving.session.get('user') + + @classmethod + def getSessionUsername(cls): + return cherrypy.serving.session.get('_cp_username') + + # Exception decorator for all exposed method calls + @classmethod + def execute(cls, func): + def decorate(*args, **kwargs): + try: + response = func(*args, **kwargs) + except DmException, ex: + cls.getLogger().error('%s' % ex) + cls.handleException(ex) + response = ex.getFullJsonRep() + except Exception, ex: + cls.getLogger().error('%s' % ex) + cls.handleException(ex) + response = InternalError(ex).getFullJsonRep() + return cls.formatJsonResponse(response) + return decorate + diff --git a/src/python/dm/common/service/dmRestWebServiceBase.py b/src/python/dm/common/service/dmRestWebServiceBase.py new file mode 100755 index 0000000000000000000000000000000000000000..dca4e9a9e2964efc58129345058bf4bc894d2239 --- /dev/null +++ b/src/python/dm/common/service/dmRestWebServiceBase.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python + +# +# Base web service class. +# + +#################################################################### +import sys +import os +import cherrypy +from cherrypy.process import plugins +from cherrypy import server + +from dm.common.constants import dmStatus +from dm.common.utility.configurationManager import ConfigurationManager +from dm.common.utility.loggingManager import LoggingManager +from dm.common.utility.dmModuleManager import DmModuleManager +from dm.common.exceptions.configurationError import ConfigurationError + +#################################################################### + +class DmRestWebServiceBase: + + DEFAULT_N_SERVER_REQUEST_THREADS = 10 + DEFAULT_SERVER_SOCKET_TIMEOUT = 30 + CONFIG_SECTION_NAME = 'WebService' + CONFIG_OPTION_NAME_LIST = [ 'serviceHost', 'servicePort', + 'sslCertFile', 'sslKeyFile', 'sslCaCertFile' ] + + class SignalHandler: + def __init__(self, signal, oldSignalHandler): + self.signal = signal + self.oldSignalHandler = oldSignalHandler + self.logger = LoggingManager.getInstance().getLogger(self.__class__.__name__) + + def signalHandler(self): + self.logger.debug('%s signal handler called' % self.signal) + DmModuleManager.getInstance().stopModules() + self.oldSignalHandler() + + def __init__(self, routeMapper): + self.configurationManager = ConfigurationManager.getInstance() + self.routeMapper = routeMapper + self.options = None + self.args = None + self.logger = None + + def prepareOptions(self): + from optparse import OptionParser + p = OptionParser() + p.add_option('-d', '--daemon', action="store_true", + dest='daemonFlag', default=False, + help="Run as a daemon.") + p.add_option('-p', '--pid-file', + dest='pidFile', default=None, + help="Store process id in the given file.") + p.add_option('', '--config-file', + dest='configFile', default=None, + help="Service configuration file.") + p.add_option('-P', '--port', + dest='servicePort', default=None, + help="Service port.") + p.add_option('-H', '--host', + dest='serviceHost', default=None, + help="Service host.") + p.add_option('-C', '--ssl-ca-cert', + dest='sslCaCertFile', default=None, + help='SSL CA certificate path (used for client SSL certificate verification). Requires --ssl-key and --ssl-cert.') + p.add_option('-c', '--ssl-cert', + dest='sslCertFile', default=None, + help='SSL certificate path. SSL operation requires both --ssl-key and --ssl-cert. Client SSL certificate verification also requires --ssl-ca-cert.') + p.add_option('-k', '--ssl-key', + dest='sslKeyFile', default=None, + help='SSL key path. SSL operation requires both --ssl-key and --ssl-cert. Client SSL certificate verification also requires --ssl-ca-cert.') + p.add_option('', '--n-server-threads', + dest='nServerThreads', default=DmRestWebServiceBase.DEFAULT_N_SERVER_REQUEST_THREADS, + help='Number of service request handler threads (defaut: %s).' % DmRestWebServiceBase.DEFAULT_N_SERVER_REQUEST_THREADS) + return p + + def initDmModules(self): + return None + + def getDefaultServerHost(self): + return None + + def getDefaultServerPort(self): + return None + + # Instantiate modified signal handler that stops dm modules first, + # and then does the default action. + def modifySignalHandlers(self, engine): + pluginsSignalHandler = plugins.SignalHandler(engine) + handlers = pluginsSignalHandler.handlers + + # Add handler for interrupt + handlers['SIGINT'] = engine.exit + + # Modify all signal handlers + for signal in handlers.keys(): + self.logger.debug('Modifying signal: %s' % signal) + oldSignalHandler = handlers[signal] + self.logger.debug('Old signal handler: %s' % oldSignalHandler) + signalHandler = DmRestWebServiceBase.SignalHandler(signal, oldSignalHandler) + self.logger.debug('Setting signal handler to: %s' % signalHandler.signalHandler) + handlers[signal] = signalHandler.signalHandler + pluginsSignalHandler.subscribe() + + def initServerLog(self): + cherrypyLogLevel = self.configurationManager.getCherrypyLogLevel() + cherrypy.log.error_log.setLevel(cherrypyLogLevel) + cherrypy.log.error_file = self.configurationManager.getCherrypyLogFile() + cherrypy.log.error_log.propagate = False + cherrypy.log.access_log.setLevel(cherrypyLogLevel) + cherrypy.log.access_file = self.configurationManager.getCherrypyAccessFile() + cherrypy.log.access_log.propagate = False + + def updateServerConfig(self): + serviceHost = self.configurationManager.getServiceHost() + servicePort = int(self.configurationManager.getServicePort()) + nServerThreads = int(self.options.nServerThreads) + configDict = { + 'server.socket_host' : serviceHost, + 'server.socket_port' : servicePort, + 'server.thread_pool' : nServerThreads, + 'log.screen' : (self.options.daemonFlag != True), + } + cherrypy.config.update(configDict) + + def readConfigFile(self, configFile): + configFile = self.options.configFile + if not configFile: + configFile = self.configurationManager.getConfigFile() + else: + self.configurationManager.setConfigFile(configFile) + + if not os.path.exists(configFile): + raise ConfigurationError('Configuration file %s does not exist.' % configFile) + # Read file and set config options + self.configurationManager.setOptionsFromConfigFile(DmRestWebServiceBase.CONFIG_SECTION_NAME, DmRestWebServiceBase.CONFIG_OPTION_NAME_LIST, configFile) + + def readCommandLineOptions(self): + # This method should be called after reading config file + # in case some options are overridden + if self.options.sslCaCertFile != None: + self.configurationManager.setSslCaCertFile(self.options.sslCaCertFile) + if self.options.sslCertFile != None: + self.configurationManager.setSslCertFile(self.options.sslCertFile) + + if self.options.sslKeyFile != None: + self.configurationManager.setSslKeyFile(self.options.sslKeyFile) + + if self.options.serviceHost != None: + self.configurationManager.setServiceHost(self.options.serviceHost) + + if self.options.servicePort != None: + self.configurationManager.setServicePort(self.options.servicePort) + + def prepareServer(self): + try: + optionParser = self.prepareOptions() + (self.options, self.args) = optionParser.parse_args() + + # Read config file and override with command line options + self.readConfigFile(self.options.configFile) + self.readCommandLineOptions() + + # Turn off console log for daemon mode. + self.logger = LoggingManager.getInstance().getLogger(self.__class__.__name__) + if self.options.daemonFlag: + LoggingManager.getInstance().setConsoleLogLevel('CRITICAL') + + dispatch = self.routeMapper.setupRoutes() + self.logger.debug('Using route dispatch: %s' % dispatch) + + config = { + '/' : { + 'request.dispatch' : dispatch, + }, + } + + # No root controller as we provided our own. + cherrypy.tree.mount(root=None, config=config) + self.initServerLog() + self.updateServerConfig() + + self.logger.info('Using host %s' % self.configurationManager.getServiceHost()) + self.logger.info('Using port %s' % self.configurationManager.getServicePort()) + self.logger.debug('Using %s request handler threads' % self.options.nServerThreads) + except Exception, ex: + if self.logger is not None: + self.logger.exception(ex) + else: + import traceback + print '\n%s' % sys.exc_info()[1] + traceback.print_exc(file=sys.stderr) + sys.exit(dmStatus.DM_ERROR) + + # Run server. + def __runServer(self): + self.logger.info('Starting service') + engine = cherrypy.engine + + # Set up Deamonization + if self.options.daemonFlag: + plugins.Daemonizer(engine).subscribe() + self.logger.debug('Daemon mode: %s' % self.options.daemonFlag) + + if self.options.pidFile != None: + plugins.PIDFile(engine, self.options.pidFile).subscribe() + self.logger.debug('Using PID file: %s' % self.options.pidFile) + + sslCertFile = self.configurationManager.getSslCertFile() + sslKeyFile = self.configurationManager.getSslKeyFile() + sslCaCertFile = self.configurationManager.getSslCaCertFile() + if sslCertFile != None and sslKeyFile != None: + server.ssl_ca_certificate = None + if sslCaCertFile != None: + server.ssl_ca_certificate = self.options.sslCaCertFile + self.logger.info('Using SSL CA cert file: %s' % sslCaCertFile) + server.ssl_certificate = sslCertFile + self.logger.info('Using SSL cert file: %s' % sslCertFile) + + server.ssl_private_key = sslKeyFile + self.logger.info('Using SSL key file: %s' % sslKeyFile) + + server.ssl_module = 'builtin' + #server.ssl_module = 'pyopenssl' + + # Increase timeout to prevent early SSL connection terminations + server.socket_timeout = DmRestWebServiceBase.DEFAULT_SERVER_SOCKET_TIMEOUT + + # Setup the signal handler to stop the application while running. + if hasattr(engine, 'signal_handler'): + self.logger.debug('Subscribing signal handler') + engine.signal_handler.subscribe() + self.modifySignalHandlers(engine) + + # Turn off autoreloader. + self.logger.debug('Turning off autoreloader') + engine.autoreload.unsubscribe() + + # Start the engine. + try: + self.logger.debug('Starting engine') + engine.start() + + # Prepare dm services. + self.logger.debug('Starting modules') + self.initDmModules() + DmModuleManager.getInstance().startModules() + + except Exception, ex: + self.logger.exception('Service exiting: %s' % ex) + DmModuleManager.getInstance().stopModules() + return dmStatus.DM_ERROR + self.logger.info('Service ready') + engine.block() + DmModuleManager.getInstance().stopModules() + self.logger.info('Service done') + return dmStatus.DM_OK + + # Run server instance. + def run(self): + self.prepareServer() + sys.exit(self.__runServer()) + +#################################################################### +# Testing + +if __name__ == '__main__': + pass diff --git a/src/python/dm/common/service/dmSessionController.py b/src/python/dm/common/service/dmSessionController.py new file mode 100755 index 0000000000000000000000000000000000000000..7ff6f009f53465ef4613aecef54551e1d725b9d4 --- /dev/null +++ b/src/python/dm/common/service/dmSessionController.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python + +# +# Base DM session controller class. +# + +####################################################################### + +import cherrypy +from dm.common.service.dmController import DmController +from dm.common.service.loginController import LoginController + +####################################################################### + +class DmSessionController(DmController): + """ Base session controller class. """ + + _cp_config = { + 'tools.sessions.on': True, + 'tools.auth.on': True + } + + #auth = LoginController() + # Add before_handler for authorization + cherrypy.tools.auth = cherrypy.Tool('before_handler', LoginController.checkAuthorization) + + def __init__(self): + DmController.__init__(self) + + @classmethod + def require(cls, *conditions): + """ + Decorator that appends conditions to the auth.require config + variable. + """ + def decorate(f): + if not hasattr(f, '_cp_config'): + f._cp_config = dict() + if 'auth.require' not in f._cp_config: + f._cp_config['auth.require'] = [] + f._cp_config['auth.require'].extend(conditions) + return f + return decorate + + @classmethod + def anyOf(cls, *conditions): + """ Returns True if any of the conditions match. """ + def check(): + for c in conditions: + if c(): + return True + return False + return check + + @classmethod + def allOf(cls, *conditions): + """ Returns True if all of the conditions match. """ + def check(): + for c in conditions: + if not c(): + return False + return True + return check + + @classmethod + def isLoggedIn(cls): + """ Returns True if session has been established. """ + def check(): + role = cherrypy.session.get(LoginController.SESSION_ROLE_KEY, None) + if role is not None: + return True + return False + return check + diff --git a/src/python/dm/common/service/impl/__init__.py b/src/python/dm/common/service/impl/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/python/dm/common/service/impl/authorizationManager.py b/src/python/dm/common/service/impl/authorizationManager.py new file mode 100755 index 0000000000000000000000000000000000000000..f5371b6259b22a6daf21106caca4acac24bbf68c --- /dev/null +++ b/src/python/dm/common/service/impl/authorizationManager.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python + +import os + +from dm.common.exceptions.authorizationError import AuthorizationError +from dm.common.objects.dmObjectManager import DmObjectManager +from dm.common.utility.configurationManager import ConfigurationManager +from dm.common.utility.objectCache import ObjectCache +from dm.common.utility.cryptUtility import CryptUtility + +class AuthorizationManager(DmObjectManager): + + DEFAULT_CACHE_SIZE = 10000 # number of items + DEFAULT_CACHE_OBJECT_LIFETIME = 3600 # seconds + + CONFIG_SECTION_NAME = 'AuthorizationManager' + ADMIN_GROUP_NAME_KEY = 'admingroupname' + PRINCIPAL_RETRIEVER_KEY = 'principalretriever' + PRINCIPAL_AUTHENTICATOR_KEY = 'principalauthenticator' + + # Get singleton instance. + @classmethod + def getInstance(cls): + from dm.common.service.impl.authorizationManager import AuthorizationManager + try: + am = AuthorizationManager() + except AuthorizationManager, ex: + am = ex + return am + + # Singleton instance. + __instance = None + + def __init__(self): + if AuthorizationManager.__instance: + raise AuthorizationManager.__instance + AuthorizationManager.__instance = self + DmObjectManager.__init__(self) + self.configurationManager = ConfigurationManager.getInstance() + self.principalRetriever = None + self.principalAuthenticatorList = [] + self.objectCache = ObjectCache(AuthorizationManager.DEFAULT_CACHE_SIZE, AuthorizationManager.DEFAULT_CACHE_OBJECT_LIFETIME) + self.configure() + + def createObjectInstance(self, moduleName, className, constructor): + self.logger.debug('Creating object: %s, %s, %s' % (moduleName, className, constructor)) + cmd = 'from %s import %s' % (moduleName, className) + exec cmd + cmd = 'objectInstance = %s' % (constructor) + exec cmd + return objectInstance + + @classmethod + def cryptPassword(cls, cleartext): + return CryptUtility.cryptPassword(cleartext) + + @classmethod + def cryptPasswordWithPbkdf2(cls, cleartext): + return CryptUtility.cryptPasswordWithPbkdf2(cleartext) + + def configure(self): + configItems = self.configurationManager.getConfigItems(AuthorizationManager.CONFIG_SECTION_NAME) + self.logger.debug('Got config items: %s' % configItems) + adminGroupName = self.configurationManager.getConfigOption(AuthorizationManager.CONFIG_SECTION_NAME, AuthorizationManager.ADMIN_GROUP_NAME_KEY) + + # Create principal retriever + principalRetriever = self.configurationManager.getConfigOption(AuthorizationManager.CONFIG_SECTION_NAME, AuthorizationManager.PRINCIPAL_RETRIEVER_KEY) + (moduleName,className,constructor) = self.configurationManager.getModuleClassConstructorTuple(principalRetriever) + self.logger.debug('Creating principal retriever class: %s' % className) + self.principalRetriever = self.createObjectInstance(moduleName, className, constructor) + self.principalRetriever.setAdminGroupName(adminGroupName) + self.logger.debug('Authorization principal retriever: %s' % (self.principalRetriever)) + + # Create principal authenticators + for (key,value) in configItems: + if key.startswith(AuthorizationManager.PRINCIPAL_AUTHENTICATOR_KEY): + (moduleName,className,constructor) = self.configurationManager.getModuleClassConstructorTuple(value) + self.logger.debug('Creating principal authenticator class: %s' % className) + principalAuthenticator = self.createObjectInstance(moduleName, className, constructor) + self.addAuthorizationPrincipalAuthenticator(principalAuthenticator) + self.logger.debug('Authorization principal authenticator: %s' % (principalAuthenticator)) + + def addAuthorizationPrincipalAuthenticator(self, principalAuthenticator): + self.principalAuthenticatorList.append(principalAuthenticator) + + def getAuthorizationPrincipal(self, username, password): + """ Get principal based on a username and password """ + # First try cache. + #self.logger.debug('Trying username %s from the cache' % username) + principal = None + principalTuple = self.objectCache.get(username) + if principalTuple is not None: + (id, principal, updateTime, expirationTime) = principalTuple + if principal is None: + # Try principal retriever + principal = self.principalRetriever.getAuthorizationPrincipal(username) + + if principal is None: + self.logger.debug('No principal for username: %s' % username) + return + + # Try all authorization principal authenticators. + for principalAuthenticator in self.principalAuthenticatorList: + self.logger.debug('Attempting to authenticate %s by %s' % (username, principalAuthenticator.getName())) + authenticatedPrincipal = principalAuthenticator.authenticatePrincipal(principal, password) + if authenticatedPrincipal is not None: + self.logger.debug('Adding authorization principal %s to the cache, authenticated by %s' % (principal.getName(),principalAuthenticator.getName())) + self.objectCache.put(username, authenticatedPrincipal) + return authenticatedPrincipal + return None + + def removeAuthorizationPrincipal(self, username): + """ Clear principal from the cache. """ + self.objectCache.remove(username) + +####################################################################### +# Testing. +if __name__ == '__main__': + am = AuthorizationManager.getInstance() + authPrincipal = am.getAuthorizationPrincipal('sveseli', 'sv') + print 'Auth principal: ', authPrincipal + + diff --git a/src/python/dm/common/service/impl/authorizationPrincipalAuthenticator.py b/src/python/dm/common/service/impl/authorizationPrincipalAuthenticator.py new file mode 100755 index 0000000000000000000000000000000000000000..7c434767ee2bdd7f8e60d59cc8e0c31563cd7b1f --- /dev/null +++ b/src/python/dm/common/service/impl/authorizationPrincipalAuthenticator.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python + +from dm.common.utility.loggingManager import LoggingManager + +class AuthorizationPrincipalAuthenticator: + + def __init__(self, name=None): + self.name = name + self.logger = LoggingManager.getInstance().getLogger(self.__class__.__name__) + + def getName(self): + return self.name + + def authenticatePrincipal(self, principal, password): + return None + +####################################################################### +# Testing. +if __name__ == '__main__': + pass + diff --git a/src/python/dm/common/service/impl/authorizationPrincipalRetriever.py b/src/python/dm/common/service/impl/authorizationPrincipalRetriever.py new file mode 100755 index 0000000000000000000000000000000000000000..935dabd6526ceed59748cc06dbe2ba035ef1c136 --- /dev/null +++ b/src/python/dm/common/service/impl/authorizationPrincipalRetriever.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +from dm.common.utility.loggingManager import LoggingManager + +class AuthorizationPrincipalRetriever: + + def __init__(self, name=None): + self.adminGroupName = None + self.name = name + self.logger = LoggingManager.getInstance().getLogger(self.__class__.__name__) + + def getName(self): + return self.name + + def setAdminGroupName(self, adminGroupName): + self.adminGroupName = adminGroupName + + def getAuthorizationPrincipal(self, username): + return None + +####################################################################### +# Testing. +if __name__ == '__main__': + pass + diff --git a/src/python/dm/common/service/impl/cryptedPasswordPrincipalAuthenticator.py b/src/python/dm/common/service/impl/cryptedPasswordPrincipalAuthenticator.py new file mode 100755 index 0000000000000000000000000000000000000000..2136e7ae634eaf30df26562cd044c9bd04a8eedd --- /dev/null +++ b/src/python/dm/common/service/impl/cryptedPasswordPrincipalAuthenticator.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +from dm.common.utility.cryptUtility import CryptUtility +from authorizationPrincipalAuthenticator import AuthorizationPrincipalAuthenticator + +class CryptedPasswordPrincipalAuthenticator(AuthorizationPrincipalAuthenticator): + + def __init__(self): + AuthorizationPrincipalAuthenticator.__init__(self, self.__class__.__name__) + + def authenticatePrincipal(self, principal, password): + if principal is not None: + principalToken = principal.getToken() + if principalToken is not None and len(principalToken): + if CryptUtility.verifyPasswordWithPbkdf2(password, principalToken): + self.logger.debug('Authentication successful for %s' % principal.getName()) + return principal + else: + self.logger.debug('Authentication failed for %s' % principal.getName()) + else: + self.logger.debug('Token is empty for %s, authentication not performed' % principal.getName()) + return None + +####################################################################### +# Testing. +if __name__ == '__main__': + pass + diff --git a/src/python/dm/common/service/impl/dbPrincipalRetriever.py b/src/python/dm/common/service/impl/dbPrincipalRetriever.py new file mode 100755 index 0000000000000000000000000000000000000000..6d887a9c5c3e650284ddd12af16d883b2da37eb4 --- /dev/null +++ b/src/python/dm/common/service/impl/dbPrincipalRetriever.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python + +from dm.common.constants import dmRole +from dm.common.objects.authorizationPrincipal import AuthorizationPrincipal +from dm.common.db.api.userDbApi import UserDbApi +from authorizationPrincipalRetriever import AuthorizationPrincipalRetriever + +class DbPrincipalRetriever(AuthorizationPrincipalRetriever): + + def __init__(self): + AuthorizationPrincipalRetriever.__init__(self, self.__class__.__name__) + self.dbApi = UserDbApi() + + def getAuthorizationPrincipal(self, username): + principal = None + try: + user = self.dbApi.getUserWithPasswordByUsername(username) + principal = AuthorizationPrincipal(username, user.get('password')) + principal.setRole(dmRole.DM_USER_ROLE) + principal.setUserInfo(user) + if self.adminGroupName is not None: + for userGroup in user.get('userGroupList', []): + if userGroup.get('name') == self.adminGroupName: + principal.setRole(dmRole.DM_ADMIN_ROLE) + except Exception, ex: + self.logger.debug(ex) + return principal + +####################################################################### +# Testing. +if __name__ == '__main__': + pass + diff --git a/src/python/dm/common/service/impl/ldapPasswordPrincipalAuthenticator.py b/src/python/dm/common/service/impl/ldapPasswordPrincipalAuthenticator.py new file mode 100755 index 0000000000000000000000000000000000000000..8ccdc7a94b7b054cba437b42dc7dd0f175f04757 --- /dev/null +++ b/src/python/dm/common/service/impl/ldapPasswordPrincipalAuthenticator.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python + +from dm.common.utility.ldapUtility import LdapUtility +from authorizationPrincipalAuthenticator import AuthorizationPrincipalAuthenticator + +class LdapPasswordPrincipalAuthenticator(AuthorizationPrincipalAuthenticator): + + def __init__(self, serverUrl, dnFormat): + AuthorizationPrincipalAuthenticator.__init__(self, self.__class__.__name__) + self.ldapUtility = LdapUtility(serverUrl, dnFormat) + + def authenticatePrincipal(self, principal, password): + if principal is not None: + try: + self.logger.debug('Checking credentials for %s' % principal.getName()) + self.ldapUtility.checkCredentials(principal.getName(), password) + return principal + except Exception, ex: + self.logger.debug(ex) + return None + +####################################################################### +# Testing. +if __name__ == '__main__': + pass + diff --git a/src/python/dm/common/service/impl/noOpPrincipalRetriever.py b/src/python/dm/common/service/impl/noOpPrincipalRetriever.py new file mode 100755 index 0000000000000000000000000000000000000000..aafef13f3435636742bbea79795396f14ad74694 --- /dev/null +++ b/src/python/dm/common/service/impl/noOpPrincipalRetriever.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +from dm.common.constants import dmRole +from dm.common.objects.authorizationPrincipal import AuthorizationPrincipal +from dm.common.utility.cryptUtility import CryptUtility +from authorizationPrincipalRetriever import AuthorizationPrincipalRetriever + +class NoOpPrincipalRetriever(AuthorizationPrincipalRetriever): + + def __init__(self): + AuthorizationPrincipalRetriever.__init__(self, self.__class__.__name__) + + def getAuthorizationPrincipal(self, username): + noOpPassword = CryptUtility.cryptPasswordWithPbkdf2(username) + principal = AuthorizationPrincipal(username, noOpPassword) + principal.setRole(dmRole.DM_USER_ROLE) + if self.adminGroupName is not None: + principal.setRole(dmRole.DM_ADMIN_ROLE) + return principal + +####################################################################### +# Testing. +if __name__ == '__main__': + pass + diff --git a/src/python/dm/common/service/loginController.py b/src/python/dm/common/service/loginController.py new file mode 100755 index 0000000000000000000000000000000000000000..4c57b874a85a6f993582ba1e86e25e17d3f67b35 --- /dev/null +++ b/src/python/dm/common/service/loginController.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python + +import cherrypy +import urllib +from cherrypy.lib import httpauth + +from dm.common.constants import dmStatus +from dm.common.constants import dmRole +from dm.common.constants import dmHttpStatus +from dm.common.exceptions.dmException import DmException +from dm.common.exceptions.dmHttpError import DmHttpError +from dm.common.exceptions.authorizationError import AuthorizationError +from dm.common.utility.loggingManager import LoggingManager +from dm.common.service.dmController import DmController +from dm.common.service.impl.authorizationManager import AuthorizationManager + +class LoginController(DmController): + """ Controller to provide login and logout actions. """ + + SESSION_USERNAME_KEY = '_cp_username' + SESSION_USER_KEY = 'user' + SESSION_ROLE_KEY = 'role' + INVALID_SESSION_KEY = 'invalidSession' + + _cp_config = { + 'tools.sessions.on' : True, + 'tools.auth.on' : True + } + + def __init__(self): + DmController.__init__(self) + + def onLogin(self, username): + """ Called on successful login. """ + return + + def onLogout(self, username): + """ Called on logout. """ + return + + @classmethod + def getLoginForm(cls, msg='Enter username and password:', username='', fromPage='/'): + return """ + <html> + <body> + <form method='post' action='/auth/login'> + <input type='hidden' name='fromPage' value='%(fromPage)s' /> + <h2>DM Service</h2> + <p/> + %(msg)s + <p/> + <table border='0'> + <tr> + <td>Username:</td><td><input type='text' name="username" value='%(username)s'/></td> + </tr> + <tr> + <td>Password:</td><td><input type='password' name='password' /></td> + </tr> + <p/> + <tr> + <td></td> + <td><input type='submit' value='Log In' /></td> + </tr> + </table> + </form> + </body> + </html>""" % locals() + + @classmethod + def parseBasicAuthorizationHeaders(cls): + try: + username = None + password = None + authorization = cherrypy.request.headers['authorization'] + authorizationHeader = httpauth.parseAuthorization(authorization) + if authorizationHeader['auth_scheme'] == 'basic': + username = authorizationHeader['username'] + password = authorizationHeader['password'] + if username and password: + return (username, password) + else: + raise AuthorizationError('Username and/or password not supplied.') + except Exception, ex: + errorMsg = 'Could not extract username/password from authorization header: %s' % ex + raise AuthorizationError(errorMsg) + + @classmethod + def checkCredentials(cls, username, password): + """ Verifies credentials for username and password.""" + logger = LoggingManager.getInstance().getLogger('LoginController:checkCredentials') + logger.debug('Checking credential for User: %s' % (username)) + #logger.debug('Checking credential for User: %s, Password: %s' % (username, password)) + logger.debug('Session id: %s' % cherrypy.serving.session.id) + principal = AuthorizationManager.getInstance().getAuthorizationPrincipal(username, password) + logger.debug('Principal: %s' % (principal)) + if principal: + cherrypy.session[LoginController.SESSION_ROLE_KEY] = principal.getRole() + logger.debug('Successful login from user: %s (role: %s)' % (username, principal.getRole())) + else: + logger.debug('Login denied for user: %s' % username) + username = cherrypy.session.get(LoginController.SESSION_USERNAME_KEY, None) + if username is not None: + cherrypy.request.login = None + cherrypy.session[LoginController.INVALID_DM_SESSION_KEY] = True + raise AuthorizationError('Incorrect username or password.') + cherrypy.session[LoginController.SESSION_USER_KEY] = principal.getUserInfo() + return principal + + @classmethod + def checkAuthorization(cls, *args, **kwargs): + """ + A tool that looks in config for 'auth.require'. If found and it + is not None, a login is required and the entry is evaluated as a list of + conditions that the user must fulfill. + """ + logger = LoggingManager.getInstance().getLogger('LoginController:checkAuthorization') + conditions = cherrypy.request.config.get('auth.require', None) + #logger.debug('Headers: %s' % (cherrypy.request.headers)) + #logger.debug('Request params: %s' % (cherrypy.request.params)) + #logger.debug('Request query string: %s' % (cherrypy.request.query_string)) + method = urllib.quote(cherrypy.request.request_line.split()[0]) + params = urllib.quote(cherrypy.request.request_line.split()[1]) + + if conditions is None: + logger.debug('No conditions imposed') + return + + sessionId = cherrypy.serving.session.id + sessionCache = cherrypy.session.cache + #logger.debug('Session: %s' % ((cherrypy.session.__dict__))) + #logger.debug('Session cache length: %s' % (len(sessionCache))) + #logger.debug('Session cache: %s' % (sessionCache)) + + # Check session. + if not sessionCache.has_key(sessionId): + errorMsg = 'Invalid or expired session id: %s.' % sessionId + logger.debug(errorMsg) + raise DmHttpError(dmHttpStatus.DM_HTTP_UNAUTHORIZED, 'User Not Authorized', AuthorizationError(errorMsg)) + + username = cherrypy.session.get(LoginController.SESSION_USERNAME_KEY) + logger.debug('Session id %s is valid (username: %s)' % (sessionId, username)) + if username: + cherrypy.request.login = username + for condition in conditions: + # A condition is just a callable that returns true or false + if not condition(): + logger.debug('Authorization check %s failed for username %s' % (condition.func_name, username)) + errorMsg = 'Authorization check %s failed for user %s.' % (condition.func_name, username) + raise DmHttpError(dmHttpStatus.DM_HTTP_UNAUTHORIZED, 'User Not Authorized', AuthorizationError(errorMsg)) + else: + logger.debug('Username is not supplied') + raise DmHttpError(dmHttpStatus.DM_HTTP_UNAUTHORIZED, 'User Not Authorized', ex) + + + @cherrypy.expose + def login(self, username=None, password=None, fromPage='/'): + self.logger.debug('Attempting login from username %s' % (username)) + try: + if username is None or password is None: + self.logger.debug('Parsing auth headers for username %s' % (username)) + (username, password) = LoginController.parseBasicAuthorizationHeaders() + self.logger.debug('Retrieving principal for username %s' % (username)) + principal = LoginController.checkCredentials(username, password) + except DmHttpError, ex: + raise + except DmException, ex: + self.logger.debug('Authorization failed (username %s): %s' % (username, ex)) + self.addDmExceptionHeaders(ex) + raise DmHttpError(dmHttpStatus.DM_HTTP_UNAUTHORIZED, 'User Not Authorized', ex) + + # Authorization worked. + cherrypy.session[LoginController.SESSION_USERNAME_KEY] = cherrypy.request.login = username + self.onLogin(username) + self.addDmSessionRoleHeaders(principal.getRole()) + self.addDmResponseHeaders() + + @cherrypy.expose + def logout(self, fromPage='/'): + sess = cherrypy.session + username = sess.get(LoginController.SESSION_USERNAME_KEY, None) + if username: + del sess[LoginController.SESSION_USERNAME_KEY] + cherrypy.request.login = None + self.onLogout(username) +