Source code for linotp.lib.config

# -*- coding: utf-8 -*-
#
#    LinOTP - the open source solution for two factor authentication
#    Copyright (C) 2010 - 2014 LSE Leading Security Experts GmbH
#
#    This file is part of LinOTP server.
#
#    This program is free software: you can redistribute it and/or
#    modify it under the terms of the GNU Affero General Public
#    License, version 3, as published by the Free Software Foundation.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU Affero General Public License for more details.
#
#    You should have received a copy of the
#               GNU Affero General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#
#    E-mail: linotp@lsexperts.de
#    Contact: www.linotp.org
#    Support: www.lsexperts.de
#
'''handle all configuration items with aspekts like persitance and
   syncronysation and provides this to all requests
'''

import logging
import time
import os
import copy

from pylons import tmpl_context as c
from linotp.config import environment as env

from linotp.lib.error import ConfigAdminError

from linotp.model import Config
from linotp.model.meta import Session

from linotp.lib.crypt import encryptPassword
from linotp.lib.crypt import decryptPassword

from datetime import datetime



log = logging.getLogger(__name__)

ENCODING = 'utf-8'


###############################################################################
# #     public interface
###############################################################################

[docs]def initLinotpConfig(): ''' return the linotpConfig class, which is integrated in the local thread context :return: thread local LinOtpConfig :rtype: LinOtpConfig Class ''' log.debug("[getLinotpConfig]") ret = getLinotpConfig() log.debug("[/getLinotpConfig]") return ret
[docs]def getLinotpConfig(): ''' return the thread local dict with all entries :return: local config dict :rtype: dict ''' ret = {} try: if False == hasattr(c, 'linotpConfig'): c.linotpConfig = LinOtpConfig() ty = type(c.linotpConfig).__name__ if ty != 'LinOtpConfig': try: c.linotpConfig = LinOtpConfig() except Exception as e: log.error("Linotp Definition Error") raise Exception(e) ret = c.linotpConfig if ret.delay == True: if hasattr(c, 'hsm') == True and isinstance(c.hsm, dict): hsm = c.hsm.get('obj') if hsm is not None and hsm.isReady() == True: ret = LinOtpConfig() c.linotpConfig = ret except Exception as e: log.debug("Bad Hack: LinotpConfig called out of controller context") ret = LinOtpConfig() if ret.delay == True: if hasattr(c, 'hsm') == True and isinstance(c.hsm, dict): hsm = c.hsm.get('obj') if hsm is not None and hsm.isReady() == True: ret = LinOtpConfig() finally: log.debug("[getLinotpConfig]") return ret ############################################################################### # # implementation class ###############################################################################
[docs]class LinOtpConfig(dict): ''' this class should be a request singleton. In case of a change, it must cover the different aspects like - env config entry and - app_globals and finally - sync this to disc ''' def __init__(self, *args, **kw): self.parent = super(LinOtpConfig, self) self.parent.__init__(*args, **kw) self.delay = False self.realms = None self.glo = getGlobalObject() conf = self.glo.getConfig() do_reload = False # do the bootstrap if no entry in the app_globals if len(conf.keys()) == 0: do_reload = True if self.glo.isConfigComplet() == False: do_reload = True self.delay = True if 'linotp.enableReplication' in conf: val = conf.get('linotp.enableReplication') if val.lower() == 'true': # # look for the timestamp when config was created e_conf_date = conf.get('linotp.Config') # # in case of replication, we always have to look if the # # config data in the database changed db_conf_date = _retrieveConfigDB('linotp.Config') if str(db_conf_date) != str(e_conf_date): do_reload = True return self.refreshConfig(do_reload=do_reload)
[docs] def refreshConfig(self, do_reload=False): conf = self.glo.getConfig() if do_reload == True: # # in case there is no entry in the dbconf or # # the config file is newer, we write the config back to the db entries = conf.keys() for entry in entries: del conf[entry] writeback = False # # get all conf entries from the config file fileconf = _getConfigFromEnv() # # get all configs from the DB (dbconf, delay) = _retrieveAllConfigDB() self.glo.setConfigIncomplete(not delay) # # we only merge the config file once as a removed entry # # might reappear otherwise if dbconf.has_key('linotp.Config') == False: conf.update(fileconf) writeback = True # # # #else: # # modCFFileDatum = fileconf.get('linotp.Config') # # dbTimeStr = dbconf.get('linotp.Config') # # dbTimeStr = dbTimeStr.split('.')[0] # # modDBFileDatum = # # datetime.strptime(dbTimeStr,'%Y-%m-%d %H:%M:%S') # # # if configFile timestamp is newer than last update: # # # reincorporate conf # # #if modCFFileDatum > modDBFileDatum: # # # conf.update(fileconf) # # # writeback = True # # conf.update(dbconf) # # chck, if there is a selfTest in the DB and delete it if dbconf.has_key('linotp.selfTest'): _removeConfigDB('linotp.selfTest') _storeConfigDB('linotp.Config', datetime.now()) # # the only thing we take from the fileconf is the selftest if fileconf.has_key('linotp.selfTest'): conf['linotp.selfTest'] = 'True' if writeback == True: for con in conf: if con != 'linotp.selfTest': _storeConfigDB(con, conf.get(con)) _storeConfigDB('linotp.Config', datetime.now()) self.glo.setConfig(conf, replace=True) self.parent.update(conf) return
[docs] def setRealms(self, realmDict): self.realms = realmDict return
[docs] def getRealms(self): return self.realms
[docs] def addEntry(self, key, val, typ=None, des=None): ''' small wrapper, as the assignement opperator has only one value argument :param key: key of the dict :type key: string :param val: any value, which is put in the dict :type val: any type :param typ: used in the database to control if the data is encrypted :type typ: None,string,password :param des: literal, which describes the data :type des: string ''' if key.startswith('linotp.') == False: key = 'linotp.' + key if type(val) in [str, unicode] and "%(here)s" in val: val = _expandHere(val) res = self.__setitem__(key, val, typ, des) return res
def __setitem__(self, key, val, typ=None, des=None): ''' implemtation of the assignement operator == internal function :param key: key of the dict :type key: string :param val: any value, which is put in the dict :type val: any type :param typ: used in the database to control if the data is encrypted :type typ: None,string,password :param des: literal, which describes the data :type des: string ''' if typ == 'password': # # in case we have a password type, we have to put # #- in the config only the encrypted pass and # #- add the config enclinotp.* with the clear password res = self.parent.__setitem__(key, encryptPassword(val)) res = self.parent.__setitem__('enc' + key, val) self.glo.setConfig({key :encryptPassword(val)}) self.glo.setConfig({'enc' + key : val}) else: # # update this config and sync with global dict and db nVal = _expandHere(val) res = self.parent.__setitem__(key, nVal) self.glo.setConfig({key:nVal}) _storeConfigDB(key, val, typ, des) _storeConfigDB('linotp.Config', datetime.now()) return res
[docs] def get(self, key, default=None): ''' check for a key in the linotp config remark: the config entries all start with linotp. if a key is not found, we do a check if there is a linotp. prefix set in the key and potetialy prepend it :param key: search value :type key: string :param default: default value, which is returned, if the value is not found :type default: any type :return: value or None :rtype: any type ''' if (self.parent.has_key(key) == False and key.startswith('linotp.') == False): key = 'linotp.' + key res = self.parent.get(key) or default return res
[docs] def has_key(self, key): res = self.parent.has_key(key) if res == False and key.startswith('linotp.') == False: key = 'linotp.' + key res = self.parent.has_key(key) if res == False and key.startswith('enclinotp.') == False: key = 'enclinotp.' + key res = self.parent.has_key(key) return res
def __delitem__(self, key): ''' remove an item from the config :param key: the name of the ocnfig entry :type key: string :return : return the std value like the std dict does, whatever this is :rtype : any value a dict update will return ''' Key = key encKey = None if self.parent.has_key(key): Key = key elif self.parent.has_key('linotp.' + key): Key = 'linotp.' + key if self.parent.has_key('enclinotp.' + key): encKey = 'enclinotp.' + key elif self.parent.has_key('enc' + key): encKey = 'enc' + key res = self.parent.__delitem__(Key) # # sync with global dict self.glo.delConfig(Key) # # do we have an decrypted in local or global dict?? if encKey is not None: res = self.parent.__delitem__(encKey) # # sync with global dict self.glo.delConfig(encKey) # # sync with db if key.startswith('linotp.'): Key = key else: Key = 'linotp.' + key _removeConfigDB(Key) _storeConfigDB('linotp.Config', datetime.now()) return res
[docs] def update(self, dic): ''' update the config dict with multiple items in a dict :param dic: dictionary of multiple items :type dic: dict :return : return the std value like the std dict does, whatever this is :rtype : any value a dict update will return ''' res = self.parent.update(dic) # # sync the lobal dict self.glo.setConfig(dic) # # sync to disc for key in dic: if key != 'linotp.Config': _storeConfigDB(key, dic.get(key)) _storeConfigDB('linotp.Config', datetime.now()) return res ############################################################################### # # helper class from here ###############################################################################
[docs]def getGlobalObject(): glo = None try: if env.config.has_key('pylons.app_globals'): glo = env.config['pylons.app_globals'] elif env.config.has_key('pylons.g'): glo = env.config['pylons.g'] except: glo = None return glo
def _getConfigReadLock(): glo = getGlobalObject() rcount = glo.setConfigReadLock() log.debug(" --------------------------------------- Read Lock %s" % rcount) def _getConfigWriteLock(): glo = getGlobalObject() rcount = glo.setConfigWriteLock() log.debug(" ------------------- ------------------ Write Lock %s" % rcount) def _releaseConfigLock(): glo = getGlobalObject() rcount = glo.releaseConfigLock() log.debug(" ------------------------------------ release Lock %s" % rcount) def _expandHere(value): log.debug('[_expandHere] value: %r' % value) Value = unicode(value) if env.config.has_key("linotp.root"): root = env.config["linotp.root"] Value = Value.replace("%(here)s", root) return Value def _getConfigFromEnv(): log.debug('[getLinotpConfig]') linotpConfig = {} try: _getConfigReadLock() for entry in env.config: # # we check for the modification time of the config file if entry == '__file__': fname = env.config.get('__file__') mTime = time.localtime(os.path.getmtime(fname)) modTime = datetime(*mTime[:6]) linotpConfig['linotp.Config'] = modTime if entry.startswith("linotp."): linotpConfig[entry] = _expandHere(env.config[entry]) if entry.startswith("enclinotp."): linotpConfig[entry] = env.config[entry] _releaseConfigLock() except Exception as e: log.error('Error while reading Config: %r' % e) _releaseConfigLock() return linotpConfig # we insert or update the key / value the config DB def _storeConfigDB(key, val, typ=None, desc=None): value = val log.debug('storeConfigDB: key %r : value %r' % (key, value)) if (not key.startswith("linotp.")): key = "linotp." + key confEntries = Session.query(Config).filter(Config.Key == unicode(key)) theConf = None if typ is not None and typ == 'password': value = encryptPassword(val) en = decryptPassword(value) if (en != val): raise Exception("StoreConfig: Error during encoding password type!") # # update if confEntries.count() == 1: theConf = confEntries[0] theConf.Value = unicode(value) if (typ is not None): theConf.Type = unicode(typ) if (desc is not None): theConf.Description = unicode(desc) # # insert elif confEntries.count() == 0: theConf = Config( Key=unicode(key), Value=unicode(value), Type=unicode(typ), Description=unicode(desc) ) if theConf is not None: Session.add(theConf) return 101 def _removeConfigDB(key): log.debug('removeConfigDB %r' % key) num = 0 if (not key.startswith("linotp.")): if not key.startswith('enclinotp.'): key = u"linotp." + key confEntries = Session.query(Config).filter(Config.Key == unicode(key)) num = confEntries.count() if num == 1: theConf = confEntries[0] try: # Session.add(theConf) Session.delete(theConf) except Exception as e: log.error('[removeConfigDB] failed') raise ConfigAdminError("remove Config failed for %r: %r" % (key, e), id=1133) return num def _retrieveConfigDB(Key): log.debug('[retrieveConfigDB] key: %r' % Key) # # prepend "lonotp." if required key = Key if (not key.startswith("linotp.")): if (not key.startswith("enclinotp.")): key = "linotp." + Key myVal = None key = u'' + key for theConf in Session.query(Config).filter(Config.Key == key): myVal = theConf.Value myVal = _expandHere(myVal) return myVal def _retrieveAllConfigDB(): config = {} delay = False for conf in Session.query(Config).all(): log.debug("[retrieveAllConfigDB] key %r:%r" % (conf.Key, conf.Value)) key = conf.Key if (not key.startswith("linotp.")): key = "linotp." + conf.Key nVal = _expandHere(conf.Value) config[key] = nVal myTyp = conf.Type if myTyp is not None: if myTyp == 'password': if hasattr(c, 'hsm') == True and isinstance(c.hsm, dict): hsm = c.hsm.get('obj') if hsm is not None and hsm.isReady() == True: config['enc' + key] = decryptPassword(conf.Value) else: delay = True return (config, delay) ########### external interfaces ###############
[docs]def storeConfig(key, val, typ=None, desc=None): log.debug('[storeConfig] %r:%r' % (key, val)) conf = getLinotpConfig() conf.addEntry(key, val, typ, desc) log.debug('[/storeConfig]') return True
[docs]def updateConfig(confi): ''' update the server config entries incl. syncing it to disc ''' log.debug('[updateConfig]') conf = getLinotpConfig() # # remember all key, which should be processed p_keys = copy.deepcopy(confi) typing = False for entry in confi: typ = confi.get(entry + ".type", None) des = confi.get(entry + ".desc", None) # # check if we have a descriptive entry if typ is not None or des is not None: typing = True if typ is not None: del p_keys[entry + ".type"] if des is not None: del p_keys[entry + ".desc"] if typing == True: # # tupple dict containing the additional info t_dict = {} for entry in p_keys: val = confi.get(entry) typ = confi.get(entry + ".type", None) des = confi.get(entry + ".desc", None) t_dict[entry] = (val, typ or "string", des) for key in t_dict: (val, typ, desc) = t_dict.get(key) if val in [str, unicode] and "%(here)s" in val: val = _expandHere(val) conf.addEntry(key, val, typ, desc) else: conf_clean = {} for key, val in confi.iteritems(): if "%(here)s" in val: val = _expandHere(val) conf_clean[key] = val conf.update(conf_clean) log.debug('[/updateConfig]') return True
[docs]def getFromConfig(key, defVal=None): log.debug('[getFromConfig] key: %s' % key) conf = getLinotpConfig() value = conf.get(key, defVal) return value
[docs]def refreshConfig(): log.debug('[refreshConfig]') conf = getLinotpConfig() conf.refreshConfig(do_reload=True) return
[docs]def removeFromConfig(key, iCase=False): log.debug('[removeFromConfig] key: %r' % key) conf = getLinotpConfig() if iCase == False: if conf.has_key(key): del conf[key] else: # # case insensitive delete # #- might have multiple hits fConf = [] for k in conf: if (k.lower() == key.lower() or k.lower() == 'linotp.' + key.lower()): fConf.append(k) if len(fConf) > 0: for k in fConf: if conf.has_key(k) or conf.has_key('linotp.' + k): del conf[k] log.debug('[/removeFromConfig]') return True #### several config functions to follow
[docs]def setDefaultMaxFailCount(maxFailCount): return storeConfig(u"DefaultMaxFailCount", maxFailCount)
[docs]def setDefaultSyncWindow(syncWindowSize): return storeConfig(u"DefaultSyncWindow", syncWindowSize)
[docs]def setDefaultCountWindow(countWindowSize): return storeConfig(u"DefaultCountWindow", countWindowSize)
[docs]def setDefaultOtpLen(otpLen): return storeConfig(u"DefaultOtpLen", otpLen)
[docs]def setDefaultResetFailCount(resetFailCount): return storeConfig(u"DefaultResetFailCount", resetFailCount) #eof###########################################################################