PageRenderTime 46ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/useridresolver/src/useridresolver/PasswdIdResolver.py

https://gitlab.com/sheshanarayanag/LinOTP
Python | 574 lines | 520 code | 1 blank | 53 comment | 2 complexity | 8eede3afbaf8f0352570d2db66c99f33 MD5 | raw file
  1. # -*- coding: utf-8 -*-
  2. #
  3. # LinOTP - the open source solution for two factor authentication
  4. # Copyright (C) 2010 - 2016 LSE Leading Security Experts GmbH
  5. #
  6. # This file is part of LinOTP userid resolvers.
  7. #
  8. # This program is free software: you can redistribute it and/or
  9. # modify it under the terms of the GNU Affero General Public
  10. # License, version 3, as published by the Free Software Foundation.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU Affero General Public License for more details.
  16. #
  17. # You should have received a copy of the
  18. # GNU Affero General Public License
  19. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. #
  21. #
  22. # E-mail: linotp@lsexperts.de
  23. # Contact: www.linotp.org
  24. # Support: www.lsexperts.de
  25. #
  26. """This module implements the communication interface
  27. for resolvin user info to the /etc/passwd user base
  28. PasswdIdResolver.IdResolver class
  29. implements the UserIdResolver for local /etc/passwd lookup
  30. Remarks:
  31. Don't use this as an enterprise solution!
  32. """
  33. import os
  34. import re
  35. import logging
  36. from . import resolver_registry
  37. from UserIdResolver import (UserIdResolver,
  38. ResolverLoadConfigError
  39. )
  40. from UserIdResolver import getResolverClass
  41. log = logging.getLogger(__name__)
  42. def str2unicode(input_str):
  43. """
  44. convert as binary string into a unicode string by trying various encodings
  45. :param input_str: input binary string
  46. :return: unicode output
  47. """
  48. output_str = input_str
  49. conversions = [{},
  50. {'encoding':'utf-8'},
  51. {'encoding':'iso-8859-1'},
  52. {'encoding':'iso-8859-15'}
  53. ]
  54. for param in conversions:
  55. try:
  56. output_str = unicode(input_str, **param)
  57. break
  58. except UnicodeDecodeError as exx:
  59. if param == conversions[-1]:
  60. log.info('no unicode conversion found for %r' % input_str)
  61. raise exx
  62. return output_str
  63. def tokenise(r):
  64. def _(s):
  65. ret = None
  66. st = s.strip()
  67. m = re.match("^" + r, st)
  68. if m:
  69. ret = (st[:m.end()].strip(), st[m.end():].strip())
  70. #ret[0].strip() ## remove ws
  71. #ret[1].strip()
  72. return ret
  73. return _
  74. @resolver_registry.class_entry('useridresolver.PasswdIdResolver.IdResolver')
  75. @resolver_registry.class_entry('useridresolveree.PasswdIdResolver.IdResolver')
  76. @resolver_registry.class_entry('useridresolver.passwdresolver')
  77. @resolver_registry.class_entry('passwdresolver')
  78. class IdResolver (UserIdResolver):
  79. fields = {"username": 1, "userid": 1,
  80. "description": 0,
  81. "phone": 0, "mobile": 0, "email": 0,
  82. "givenname": 0, "surname": 0, "gender": 0
  83. }
  84. searchFields = {
  85. "username": "text",
  86. "userid": "numeric",
  87. "description": "text",
  88. "email": "text"
  89. }
  90. sF = {
  91. "username": 0,
  92. "cryptpass": 1,
  93. "userid": 2,
  94. "description": 4,
  95. "email": 4,
  96. }
  97. @classmethod
  98. def setup(cls, config=None, cache_dir=None):
  99. '''
  100. this setup hook is triggered, when the server
  101. starts to serve the first request
  102. :param config: the linotp config
  103. :type config: the linotp config dict
  104. '''
  105. log.info("Setting up the PasswdResolver")
  106. return
  107. def __init__(self):
  108. """
  109. simple constructor
  110. """
  111. self.name = "etc-passwd"
  112. self.fileName = ""
  113. self.name = "P"
  114. self.nameDict = {}
  115. self.descDict = {}
  116. self.reversDict = {}
  117. self.passDict = {}
  118. self.officePhoneDict = {}
  119. self.homePhoneDict = {}
  120. self.surnameDict = {}
  121. self.givennameDict = {}
  122. self.emailDict = {}
  123. def close(self):
  124. """
  125. request hook - to close down resolver object
  126. """
  127. return
  128. def loadFile(self):
  129. """
  130. init loads the /etc/passwd
  131. user and uid as a dict for /
  132. user loginname lookup
  133. """
  134. if (self.fileName == ""):
  135. self.fileName = "/etc/passwd"
  136. log.info('[loadFile] loading users from file %s' % (self.fileName))
  137. fileHandle = open(self.fileName, "r")
  138. line = fileHandle.readline()
  139. ID = self.sF["userid"]
  140. NAME = self.sF["username"]
  141. PASS = self.sF["cryptpass"]
  142. DESCRIPTION = self.sF["description"]
  143. while line:
  144. line = line.strip()
  145. if len(line) == 0:
  146. continue
  147. line = str2unicode(line)
  148. fields = line.split(":", 7)
  149. self.nameDict["%s" % fields[NAME]] = fields[ID]
  150. ## for speed reason - build a revers lookup
  151. self.reversDict[fields[ID]] = "%s" % fields[NAME]
  152. ## for full info store the line
  153. self.descDict[fields[ID]] = fields
  154. ## store the crypted password
  155. self.passDict[fields[ID]] = fields[PASS]
  156. ## store surname, givenname and phones
  157. descriptions = fields[DESCRIPTION].split(",")
  158. name = descriptions[0]
  159. names = name.split(' ', 1)
  160. self.givennameDict[fields[ID]] = names[0]
  161. self.surnameDict[fields[ID]] = ""
  162. self.officePhoneDict[fields[ID]] = ""
  163. self.homePhoneDict[fields[ID]] = ""
  164. self.emailDict[fields[ID]] = ""
  165. if len(names) >= 2:
  166. self.surnameDict[fields[ID]] = names[1]
  167. if len(descriptions) >= 4:
  168. self.officePhoneDict[fields[ID]] = descriptions[2]
  169. self.homePhoneDict[fields[ID]] = descriptions[3]
  170. if len(descriptions) >= 5:
  171. for field in descriptions[4:]:
  172. # very basic e-mail regex
  173. email_match = re.search('.+@.+\..+', field)
  174. if email_match:
  175. self.emailDict[fields[ID]] = email_match.group(0)
  176. """ print ">>" + key[0] + "<< " + key[2] """
  177. line = fileHandle.readline()
  178. def checkPass(self, uid, password):
  179. """
  180. This function checks the password for a given uid.
  181. - returns true in case of success
  182. - false if password does not match
  183. We do not support shadow passwords at the moment. so the seconds column
  184. of the passwd file needs to contain the crypted password
  185. """
  186. import crypt
  187. if type(password) is unicode:
  188. log.debug("Password is a unicode string. Encoding to UTF-8 for \
  189. crypt.crypt() function.")
  190. password = password.encode('utf-8')
  191. log.info("[checkPass] checking password for user uid %s" % uid)
  192. cryptedpasswd = self.passDict[uid]
  193. log.debug("[checkPass] We found the crypted pass %s for uid %s"
  194. % (cryptedpasswd, uid))
  195. if cryptedpasswd:
  196. if cryptedpasswd == 'x' or cryptedpasswd == '*':
  197. err = "Sorry, currently no support for shadow passwords"
  198. log.error("[checkPass] %s " % err)
  199. raise NotImplementedError(err)
  200. cp = crypt.crypt(password, cryptedpasswd)
  201. log.debug("[checkPass] crypted pass is %s" % cp)
  202. if crypt.crypt(password, cryptedpasswd) == cryptedpasswd:
  203. log.info("[checkPass] successfully authenticated user uid %s"
  204. % uid)
  205. return True
  206. else:
  207. log.warning("[checkPass] user uid %s failed to authenticate"
  208. % uid)
  209. return False
  210. else:
  211. log.warning("[checkPass] Failed to verify password. "
  212. "No crypted password found in file")
  213. return False
  214. def getUserInfo(self, userId, no_passwd=False):
  215. """
  216. get some info about the user
  217. as we only have the loginId, we have to traverse the dict for the value
  218. :param userId: the to be searched user
  219. :param no_passwd: retrun no password
  220. :return: dict of user info
  221. """
  222. ret = {}
  223. if userId in self.reversDict:
  224. fields = self.descDict.get(userId)
  225. for key in self.sF:
  226. if no_passwd and key == "cryptpass":
  227. continue
  228. index = self.sF[key]
  229. ret[key] = fields[index]
  230. ret['givenname'] = self.givennameDict.get(userId)
  231. ret['surname'] = self.surnameDict.get(userId)
  232. ret['phone'] = self.homePhoneDict.get(userId)
  233. ret['mobile'] = self.officePhoneDict.get(userId)
  234. ret['email'] = self.emailDict.get(userId)
  235. return ret
  236. def getUsername(self, userId):
  237. '''
  238. ## TODO: why does this return bool
  239. :param userId: the user to be searched
  240. :return: true, if a user id exists
  241. '''
  242. return userId in self.reversDict
  243. def getUserId(self, LoginName):
  244. """
  245. search the user id from the login name
  246. we need the encoding no more as the input is converted to unicode
  247. by the str2unicode function
  248. :param LoginName: the login of the user
  249. :return: the userId
  250. """
  251. return self.nameDict.get(LoginName, '') or ''
  252. def getSearchFields(self, searchDict=None):
  253. """
  254. show, which search fields this userIdResolver supports
  255. TODO: implementation is not completed
  256. :param searchDict: fields, which should be queried
  257. :return: dict of all searchFields
  258. """
  259. if searchDict != None:
  260. for search in searchDict:
  261. pattern = searchDict[search]
  262. log.debug("[getSearchFields] searching for %s:%s",
  263. search, pattern)
  264. return self.searchFields
  265. def getUserList(self, searchDict):
  266. """
  267. get a list of all users matching the search criteria of the searchdict
  268. :param searchDict: dict of search expressions
  269. """
  270. ret = []
  271. ## first check if the searches are in the searchDict
  272. for l in self.descDict:
  273. line = self.descDict[l]
  274. ok = True
  275. for search in searchDict:
  276. if not search in self.searchFields:
  277. ok = False
  278. break
  279. pattern = searchDict[search]
  280. log.debug("[getUserList] searching for %s:%s", search, pattern)
  281. if search == "username":
  282. ok = self.checkUserName(line, pattern)
  283. elif search == "userid":
  284. ok = self.checkUserId(line, pattern)
  285. elif search == "description":
  286. ok = self.checkDescription(line, pattern)
  287. elif search == "email":
  288. ok = self.checkEmail(line, pattern)
  289. if ok != True:
  290. break
  291. if ok == True:
  292. uid = line[self.sF["userid"]]
  293. info = self.getUserInfo(uid, no_passwd=True)
  294. ret.append(info)
  295. return ret
  296. def checkUserName(self, line, pattern):
  297. """
  298. check for user name
  299. """
  300. username = line[self.sF["username"]]
  301. ret = self.stringMatch(username, pattern)
  302. return ret
  303. def checkDescription(self, line, pattern):
  304. description = line[self.sF["description"]]
  305. ret = self.stringMatch(description, pattern)
  306. return ret
  307. def checkEmail(self, line, pattern):
  308. email = line[self.sF["email"]]
  309. ret = self.stringMatch(email, pattern)
  310. return ret
  311. def stringMatch(self, cString, cPattern):
  312. ret = False
  313. e = s = ""
  314. string = cString.lower()
  315. pattern = cPattern.lower()
  316. if pattern.startswith("*"):
  317. e = "e"
  318. pattern = pattern[1:]
  319. if pattern.endswith("*"):
  320. s = "s"
  321. pattern = pattern[:-1]
  322. if (e == "e" and s == "s"):
  323. if string.find(pattern) != -1:
  324. return True
  325. elif (e == "e"):
  326. if string.endswith(pattern):
  327. return True
  328. elif (s == "s"):
  329. if string.startswith(pattern):
  330. return True
  331. else:
  332. if string == pattern:
  333. return True
  334. return ret
  335. def checkUserId(self, line, pattern):
  336. """
  337. check for the userId
  338. """
  339. ret = False
  340. try:
  341. cUserId = int(line[self.sF["userid"]])
  342. except:
  343. return ret
  344. (op, val) = tokenise(">=|<=|>|<|=|between")(pattern)
  345. if op == "between":
  346. (lVal, hVal) = val.split(",", 2)
  347. try:
  348. ilVal = int(lVal.strip())
  349. ihVal = int(hVal.strip())
  350. if ihVal < ilVal:
  351. v = ihVal
  352. ihVal = ilVal
  353. ilVal = v
  354. except:
  355. return ret
  356. if (cUserId <= ihVal and cUserId >= ilVal):
  357. ret = True
  358. else:
  359. try:
  360. ival = int(val)
  361. except:
  362. return ret
  363. if op == "=":
  364. if (cUserId == ival):
  365. ret = True
  366. elif op == ">":
  367. if (cUserId > ival):
  368. ret = True
  369. elif op == ">=":
  370. if (cUserId >= ival):
  371. ret = True
  372. elif op == "<":
  373. if (cUserId < ival):
  374. ret = True
  375. elif op == "<=":
  376. if (cUserId < ival):
  377. ret = True
  378. return ret
  379. #############################################################
  380. # server info methods
  381. #############################################################
  382. def getResolverId(self):
  383. """ getResolverId(LoginName)
  384. - returns the resolver identifier string
  385. - empty string if not exist
  386. """
  387. return self.fileName
  388. @classmethod
  389. def getResolverClassType(cls):
  390. return 'passwdresolver'
  391. def getResolverType(self):
  392. return IdResolver.getResolverClassType()
  393. @classmethod
  394. def getResolverClassDescriptor(cls):
  395. '''
  396. return the descriptor of the resolver, which is
  397. - the class name and
  398. - the config description
  399. :return: resolver description dict
  400. :rtype: dict
  401. '''
  402. descriptor = {}
  403. typ = cls.getResolverClassType()
  404. descriptor['clazz'] = "useridresolver.PasswdIdResolver.IdResolver"
  405. descriptor['config'] = {'fileName': 'string'}
  406. return {typ: descriptor}
  407. def getResolverDescriptor(self):
  408. return IdResolver.getResolverClassDescriptor()
  409. def getConfigEntry(self, config, key, conf, required=True):
  410. ckey = key
  411. cval = ""
  412. if conf != "" or None:
  413. ckey = ckey + "." + conf
  414. if ckey in config:
  415. cval = config[ckey]
  416. if cval == "":
  417. if key in config:
  418. cval = config[key]
  419. if cval == "" and required == True:
  420. raise Exception("missing config entry: " + key)
  421. return cval
  422. def loadConfig(self, config, conf):
  423. """ loadConfig(configDict)
  424. The UserIdResolver could be configured
  425. from the pylon app config - here
  426. this could be the passwd file ,
  427. whether it is /etc/passwd or /etc/shadow
  428. """
  429. fileName = self.getConfigEntry(config,
  430. 'linotp.passwdresolver.fileName', conf)
  431. fileName = os.path.realpath(fileName)
  432. if (not os.path.isfile(fileName) or not os.access(fileName, os.R_OK)):
  433. raise ResolverLoadConfigError('File %r does not exist or is not '
  434. 'accesible' % fileName)
  435. self.fileName = fileName
  436. self.loadFile()
  437. return self
  438. if __name__ == "__main__":
  439. print " PasswdIdResolver - IdResolver class test "
  440. y = getResolverClass("PasswdIdResolver", "IdResolver")()
  441. y.loadConfig({'linotp.passwdresolver.fileName': '/etc/passwd'}, "")
  442. x = getResolverClass("PasswdIdResolver", "IdResolver")()
  443. x.loadConfig({'linotp.passwdresolver.fileName': '/etc/meinpass'}, "")
  444. print "======/etc/meinpass=========="
  445. print x.getUserList({'username': '*', "userid": ">= 1000"})
  446. print "======/etc/passwd=========="
  447. print y.getUserList({'username': '*', "userid": ">= 1000"})
  448. print "================"
  449. user = "koelbel"
  450. loginId = y.getUserId(user)
  451. print " %s - %s" % (user, loginId)
  452. print " reId - " + y.getResolverId()
  453. ret = y.getUserInfo(loginId)
  454. print "result %r" % ret
  455. ret = y.getSearchFields()
  456. #ret["username"]="^bea*"
  457. search = {
  458. "userid": " between 1000, 1005",
  459. # "username":"^bea*",
  460. #"description":"*Audio*",
  461. # "descriptio":"*Winkler*",
  462. # "userid":" <=1003",
  463. }
  464. #
  465. ret = y.getUserList(search)
  466. print ret