# -*- 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
#
""" resolver objects and processing """
import logging
import re
import copy
from linotp.lib.context import context
from linotp.lib.config import storeConfig
from linotp.lib.config import getGlobalObject
from linotp.lib.config import removeFromConfig
from linotp.lib.config import getLinotpConfig
from linotp.lib.util import getParam
from linotp.lib.crypt import decryptPassword
required = True
optional = False
__all__ = [ 'defineResolver', 'checkResolverType', 'splitResolver',
'getResolverList', 'getResolverInfo', 'deleteResolver',
'getResolverObject', 'initResolvers', 'closeResolvers',
'setupResolvers'
]
log = logging.getLogger(__name__)
class Resolver():
"""
helper class to define a new resolver
"""
def __init__(self, name=None):
self.name = name
self.type = None
self.data = {}
self.types = {}
self.desc = {}
def getDefinition(self, param):
self.name = getParam(param, 'resolver', required)
return getResolverInfo(self.name)
def setDefinition(self, param):
'''
handle name
'''
self.name = getParam(param, 'name', required)
# We should have no \.
# This only leads to problems.
nameExp = "^[A-Za-z0-9_\-]+$"
if re.match(nameExp, self.name) is None:
e = Exception("non conformant characters in resolver name: " + self.name + " (not in " + nameExp + ")")
raise e
# handle types
self.type = getParam(param, 'type', required)
resolvertypes = get_resolver_types()
if self.type not in resolvertypes:
e = Exception("resolver type : %s not in %s" %
(self.type, unicode(resolvertypes)))
raise e
resolvers = getResolverList(filter_resolver_type=self.type)
for resolver in resolvers:
if self.name.lower() == resolver.lower():
if self.name == resolver:
continue
e = Exception("resolver with similar name already exists: %s" %
(resolver))
raise e
resolver_config = get_resolver_classConfig(self.type)
if self.type in resolver_config:
config = resolver_config.get(self.type).get('config', {})
else:
config = resolver_config
for k in param:
if k != 'name' and k != 'type':
if k.startswith('type.') == True:
key = k[len('type.'):]
self.types[key] = param.get(k)
elif k.startswith('desc.') == True:
key = k[len('desc.'):]
self.desc[key] = param.get(k)
elif 'session' == k:
# supress session parameter
pass
else:
self.data[k] = param.get(k)
if k in config:
self.types[k] = config.get(k)
else:
log.warn("[setDefinition]: the passed key %r is not a "
"parameter for the resolver %r" % (k, self.type))
# now check if we have for every type def an parameter
ok = self._sanityCheck()
if ok != True:
raise Exception("type definition does not match parameter! %s"
% unicode(param))
return
def _sanityCheck(self):
ret = True
for t in self.types:
if self.data.has_key(t) == False:
ret = False
for t in self.desc:
if self.data.has_key(t) == False:
ret = False
return ret
def saveConfig(self):
res = 'success'
if self.name is None:
return "no resolver name defined"
# do the setConfig()'s
prefix = self.type + "."
postfix = "." + self.name
for d in self.data:
key = prefix + d + postfix
val = self.data.get(d)
typ = None
desc = None
if self.types.has_key(d) == True:
typ = self.types.get(d)
if self.desc.has_key(d) == True:
desc = self.desc.get(d)
res = storeConfig(key, val, typ, desc)
return res
[docs]def defineResolver(params):
"""
set up a new resolver from request parameters
:param params: dict of request parameters
"""
resolver = Resolver()
resolver.setDefinition(params)
res = resolver.saveConfig()
return res
[docs]def checkResolverType(resolver):
"""
check if a resolver of the given type exists
:param resolver: full qualified resolver name
or optional with trailing conf like:
useridresolver.PasswdIdResolver.IdResolver.etc_resl
:return: True or False
"""
res = False
ret = False
# prepare
reso = resolver.strip()
reso = reso.replace("\"", "")
# the fully qualified resolver
if reso in context.resolver_clazzes:
res = context.resolver_clazzes.get(reso)
ret = True
else:
# if the last argument is the configuration
pack = reso.split('.')
rtype = ".".join(pack[:-1])
conf = pack[-1]
# lookup, if there is a resolver definition
if rtype in context.resolver_types:
res = "%s.%s" % (rtype, conf)
ret = True
# #
else:
# legacy support, where resolver is defined as
# "useridresolver.passwdresolver.mrealm"
# so we only could rely only on the type definition e.g.
# 'passwdresolver' as part of the string
for res_id, res_type in context.resolver_types.iteritems():
if res_type in reso:
res = "%s.%s" % (res_id, conf)
ret = True
break
# is resolver defined in the linotp config
try:
getResolverObject(res)
except Exception as exx:
log.warning("Failed to setup resolver %r: %r" % (res, exx))
res = False
ret = False
return (ret, res)
#### helper functions to retrieve information from the UserIDResolvers ###################
[docs]def splitResolver(resolver):
reso = resolver.strip()
reso = reso.replace("\"", "")
try:
l = reso.split('.', 3)
package = l[0]
if package == 'useridresolveree':
package = 'useridresolver'
module = l[1]
class_ = l[2]
if len(l) == 3:
conf = ""
elif len(l) == 4: # all the rest
conf = l[3]
except Exception as e:
log.error("[splitResolver] split of resolver failed %s : %r " % (reso, e))
raise Exception("invalid resolver class specification" + reso)
return (package, module, class_, conf)
# external system/getResolvers
[docs]def getResolverList(filter_resolver_type=None):
'''
Gets the list of configured resolvers
:param filter_resolver_type: Only resolvers of the given type are returned
:type filter_resolver_type: string
:rtype: Dictionary of the resolvers and their configuration
'''
Resolvers = {}
resolvertypes = get_resolver_types()
conf = getLinotpConfig()
for entry in conf:
for typ in resolvertypes:
if entry.startswith("linotp." + typ):
# the realm might contain dots "."
# so take all after the 3rd dot for realm
r = {}
resolver = entry.split(".", 3)
# An old entry without resolver name
if len(resolver) <= 3:
break
r["resolvername"] = resolver[3]
r["entry"] = entry
r["type"] = typ
if (filter_resolver_type is None) or (filter_resolver_type and filter_resolver_type == typ):
Resolvers[resolver[3]] = r
# Dont check the other resolver types
break
return Resolvers
[docs]def getResolverInfo(resolvername):
'''
return the resolver info of the given resolvername
:param resolvername: the requested resolver
:type resolvername: string
:return : dict of resolver description
'''
resolver_dict = {}
typ = ""
resolvertypes = get_resolver_types()
descr = {}
conf = getLinotpConfig()
for entry in conf:
for typ in resolvertypes:
# get the typed values of the descriptor!
resolver_conf = get_resolver_classConfig(typ)
if typ in resolver_conf:
descr = resolver_conf.get(typ).get('config', {})
else:
descr = resolver_conf
if entry.startswith("linotp." + typ) and entry.endswith(resolvername):
# the realm might contain dots "."
# so take all after the 3rd dot for realm
resolver = entry.split(".", 3)
# An old entry without resolver name
if len(resolver) <= 3:
break
value = conf.get(entry)
if resolver[2] in descr:
configEntry = resolver[2]
if descr.get(configEntry) == 'password':
# do we already have the decrypted pass?
if 'enc' + entry in conf:
value = conf.get('enc' + entry)
else:
# if no, we take the encpass and decrypt it
value = conf.get(entry)
try:
en = decryptPassword(value)
value = en
except:
log.info("Decryption of resolver passwd failed: compatibility issue?")
resolver_dict[ resolver[2] ] = value
# Dont check the other resolver types
break
return { "type" : typ, "data" : resolver_dict, "resolver" : resolvername}
[docs]def deleteResolver(resolvername):
'''
delete a resolver and all related config entries
:paramm resolvername: the name of the to be deleted resolver
:type resolvername: string
:return: sucess or fail
:rtype: boelean
'''
res = False
resolvertypes = get_resolver_types()
conf = getLinotpConfig()
delEntries = []
for entry in conf:
rest = entry.split(".", 3)
lSplit = len(rest)
if lSplit > 3:
rConf = rest[lSplit - 1]
if rConf == resolvername:
if rest[0] == "linotp" or rest[0] == "enclinotp" :
typ = rest[1]
if typ in resolvertypes:
delEntries.append(entry)
if len(delEntries) > 0 :
try:
for entry in delEntries:
res = removeFromConfig(entry)
log.debug("[deleteResolver] removing key: %s" % entry)
res = True
except Exception as e:
log.error("deleteResolver: %r" % e)
res = False
return res
# external in token.py user.py validate.py
[docs]def getResolverObject(resolvername):
"""
get the resolver instance from a resolver name spec
- either take the class from the request context
- or create one from the global object list + init with resolver config
:remark: the resolver object is preserved in the request context, so that
a resolver could preserve a connection durung a request
:param resolvername: the resolver string as from the token including
the config as last part
:return: instance of the resolver with the loaded config
"""
r_obj = None
# this patch is a bit hacky:
# the normal request has a request context, where it retrieves
# the resolver info from and preserves the loaded resolvers for reusage
# But in case of a authentication request (by a redirect from a 401)
# the caller is no std request and the context object is missing :-(
# The solution is to deal with local references, either to the
# global context or to local data (where we have no reuse of the resolver)
resolvers_loaded = {}
try:
if hasattr(context, 'resolvers_loaded') == False:
setattr(context, 'resolvers_loaded', {})
resolvers_loaded = context.resolvers_loaded
except Exception as exx:
resolvers_loaded = {}
# port of the 2.6. resolver to 2.7
if resolvername[:len('useridresolveree.')] == 'useridresolveree.':
resolvername = "useridresolver.%s" % resolvername[len('useridreseolveree.') - 1:]
# test if there is already a resolver of this kind loaded
if resolvername in resolvers_loaded:
return resolvers_loaded.get(resolvername)
# no resolver - so instatiate one
else:
parts = resolvername.split('.')
if len(parts) > 2:
re_name = '.'.join(parts[:-1])
r_obj_class = get_resolver_class(re_name)
if r_obj_class is None:
log.error("unknown resolver class %s " % resolvername)
return r_obj
# create the resolver instance and load the config
r_obj = r_obj_class()
conf = resolvername.split(".")[-1]
if r_obj is not None:
config = getLinotpConfig()
r_obj.loadConfig(config, conf)
resolvers_loaded[resolvername] = r_obj
return r_obj
# external lib/base.py
[docs]def setupResolvers(config=None, cache_dir="/tmp"):
"""
hook for the server start -
initialize the resolvers
"""
glo = getGlobalObject()
resolver_clazzes = copy.deepcopy(glo.getResolverClasses())
for resolver_clazz in resolver_clazzes.values():
if hasattr(resolver_clazz, 'setup'):
try:
resolver_clazz.setup(config=config, cache_dir=cache_dir)
except Exception as exx:
log.error("failed to call setup of %r" % resolver_clazz)
return
[docs]def initResolvers():
"""
hook for the request start -
create a deep copy of the dict with the global resolver classes
"""
try:
glo = getGlobalObject()
resolver_clazzes = copy.deepcopy(glo.getResolverClasses())
setattr(context, 'resolver_clazzes', resolver_clazzes)
resolver_types = copy.deepcopy(glo.getResolverTypes())
setattr(context, 'resolver_types', resolver_types)
# dict of all resolvers, which are instatiated during the request
setattr(context, 'resolvers_loaded', {})
except Exception as exx:
log.error("Failed to initialize resolver in context %r" % exx)
return
# external lib/base.py
[docs]def closeResolvers():
"""
hook to close the resolvers at the end of the request
"""
if hasattr(context, 'resolvers_loaded'):
try:
for resolver in context.resolvers_loaded.values():
if hasattr(resolver, 'close'):
resolver.close()
except Exception as exx:
log.error("Failed to close resolver in context %r" % exx)
return
# internal functions
def get_resolver_class(resolver_type):
'''
return the class object for a resolver type
:param resolver_type: string specifying the resolver
fully qualified or abreviated
:return: resolver object class
'''
ret = None
# ## this patch is a bit hacky:
# the normal request has a request context, where it retrieves
# the resolver info from and preserves the loaded resolvers for reusage
# But in case of a authentication request (by a redirect from a 401)
# the caller is no std request and the context object is missing :-(
# The solution is, to deal with local references, either to the
# global context or to local data
try:
resolver_clazzes = context.resolver_clazzes
resolver_types = context.resolver_types
except Exception as exx:
glo = getGlobalObject()
resolver_clazzes = copy.deepcopy(glo.getResolverClasses())
resolver_types = copy.deepcopy(glo.getResolverTypes())
parts = resolver_type.split('.')
# resolver is fully qualified
if len(parts) > 1:
if resolver_type in resolver_clazzes:
ret = resolver_clazzes.get(resolver_type)
# resolver is in abreviated form, we have to do a reverse lookup
elif resolver_type in resolver_types.values():
for k, v in resolver_types.iteritems():
if v == resolver_type:
ret = resolver_clazzes.get(k, None)
break
if ret is None:
pass
return ret
def get_resolver_types():
"""
get the array of the registred resolvers
:return: array of resolvertypes like 'passwdresolver'
"""
return context.resolver_types.values()
def get_resolver_classConfig(claszzesType):
"""
get the configuration description of a resolver
:param claszzesType: literal resolver type
:return: configuration description dict
"""
descriptor = None
resolver_class = get_resolver_class(claszzesType)
if resolver_class is not None:
descriptor = resolver_class.getResolverClassDescriptor()
return descriptor
#eof###########################################################################