Advertisement
Guest User

Untitled

a guest
Oct 22nd, 2018
95
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 62.44 KB | None | 0 0
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2002-2018 University of Oslo, Norway
  3. #
  4. # This file is part of Cerebrum.
  5. #
  6. # Cerebrum is free software; you can redistribute it and/or modify it
  7. # under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation; either version 2 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # Cerebrum is distributed in the hope that it will be useful, but
  12. # WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. # General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with Cerebrum; if not, write to the Free Software Foundation,
  18. # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
  19.  
  20. """The Account module stores information about an account of
  21. arbitrary type. Extentions like PosixUser are used for additional
  22. parameters that may be required by the requested backend.
  23.  
  24. Usernames are stored in the table entity_name. The domain that the
  25. default username is stored in is yet to be determined.
  26. """
  27. from __future__ import unicode_literals
  28.  
  29. import crypt
  30. import functools
  31. import string
  32. import mx
  33. import hashlib
  34. import base64
  35.  
  36. import six
  37.  
  38. from Cerebrum import Utils, Disk
  39. from Cerebrum.Entity import (EntityName,
  40. EntityQuarantine,
  41. EntityContactInfo,
  42. EntityExternalId,
  43. EntitySpread)
  44. from Cerebrum import Errors
  45. from Cerebrum.Utils import (NotSet,
  46. argument_to_sql,
  47. prepare_string)
  48. from Cerebrum.utils.username import suggest_usernames
  49. from Cerebrum.modules.pwcheck.checker import (check_password,
  50. PasswordNotGoodEnough)
  51. from Cerebrum.modules.password_generator.generator import PasswordGenerator
  52.  
  53. import cereconf
  54.  
  55.  
  56. class AccountType(object):
  57. """The AccountType class does not use populate logic as the only
  58. data stored represent a PK in the database"""
  59.  
  60. def get_account_types(self, all_persons_types=False, owner_id=None,
  61. filter_expired=True):
  62. """Return dbrows of account_types for the given account.
  63.  
  64. @type all_persons_types: bool
  65. @param all_persons_types: If True, returns all the account_types by the
  66. owner's (person)'s accounts, and not just this account's types.
  67.  
  68. @type owner_id: int
  69. @param owner_id: If set, returns all the account_types for all accounts
  70. that has the given owner_id as their owner.
  71.  
  72. @type filter_expired: bool
  73. @param filter_expired: If expired accounts should be ignored. Note that
  74. this does not check if the affiliation or account_type is expired!
  75.  
  76. @rtype: db-rows
  77. @return: Each row is an account type, containing the person_id, ou_id,
  78. affiliation, account_id and priority.
  79.  
  80. """
  81. if all_persons_types or owner_id is not None:
  82. col = 'person_id'
  83. if owner_id is not None:
  84. val = owner_id
  85. else:
  86. val = self.owner_id
  87. else:
  88. col = 'account_id'
  89. val = self.entity_id
  90. tables = ["[:table schema=cerebrum name=account_type] at"]
  91. where = ["at.%s = :%s" % (col, col)]
  92. if filter_expired:
  93. tables.append("[:table schema=cerebrum name=account_info] ai")
  94. where.append("""(ai.account_id = at.account_id AND
  95. (ai.expire_date IS NULL OR
  96. ai.expire_date > [:now]))""")
  97. return self.query("""
  98. SELECT person_id, ou_id, affiliation, at.account_id, priority
  99. FROM """ + ", ".join(tables) + """
  100. WHERE """ + " AND ".join(where) + """
  101. ORDER BY priority""",
  102. {col: val})
  103.  
  104. def set_account_type(self, ou_id, affiliation, priority=None):
  105. """Insert or update the given account type.
  106.  
  107. :type priority: int
  108. :param priority:
  109. Priorities the account type if the account has more than one. The
  110. highest priority, i.e. lowest value, is the primary affiliation.
  111. Defaults to 5 higher than highest value amongst existing account
  112. type priorities.
  113.  
  114. :rtype: tuple
  115. """
  116. all_pris = {}
  117. orig_pri = None
  118. max_pri = 0
  119. for row in self.get_account_types(all_persons_types=True,
  120. filter_expired=False):
  121. all_pris[int(row['priority'])] = row
  122. if(ou_id == row['ou_id'] and affiliation == row['affiliation'] and
  123. self.entity_id == row['account_id']):
  124. orig_pri = row['priority']
  125. if row['priority'] > max_pri:
  126. max_pri = row['priority']
  127. if priority is None:
  128. priority = max_pri + 5
  129. if orig_pri is None:
  130. if priority in all_pris:
  131. self._set_account_type_priority(
  132. all_pris, priority, priority + 1)
  133. cols = {'person_id': int(self.owner_id),
  134. 'ou_id': int(ou_id),
  135. 'affiliation': int(affiliation),
  136. 'account_id': int(self.entity_id),
  137. 'priority': priority}
  138. self.execute("""
  139. INSERT INTO [:table schema=cerebrum name=account_type] (%(tcols)s)
  140. VALUES (%(binds)s)""" % {'tcols': ", ".join(cols.keys()),
  141. 'binds': ", ".join(
  142. [":%s" % t for t in cols.keys()])},
  143. cols)
  144. self._db.log_change(self.entity_id, self.const.account_type_add,
  145. None, change_params={
  146. 'ou_id': int(ou_id),
  147. 'affiliation': int(affiliation),
  148. 'priority': int(priority)})
  149. return 'add', priority
  150. else:
  151. if orig_pri != priority:
  152. self._set_account_type_priority(all_pris, orig_pri, priority)
  153. return 'mod', priority
  154.  
  155. def _set_account_type_priority(self, all_pris, orig_pri, new_pri):
  156. """Recursively insert the new priority, increasing parent
  157. priority with one if there is a conflict"""
  158. if new_pri in all_pris:
  159. self._set_account_type_priority(all_pris, new_pri, new_pri + 1)
  160. orig_pri = int(orig_pri)
  161. cols = {'person_id': all_pris[orig_pri]['person_id'],
  162. 'ou_id': all_pris[orig_pri]['ou_id'],
  163. 'affiliation': all_pris[orig_pri]['affiliation'],
  164. 'account_id': all_pris[orig_pri]['account_id'],
  165. 'priority': new_pri}
  166. self.execute("""
  167. UPDATE [:table schema=cerebrum name=account_type]
  168. SET priority=:priority
  169. WHERE %s""" % " AND ".join(["%s=:%s" % (x, x)
  170. for x in cols.keys() if x != "priority"]),
  171. cols)
  172. self._db.log_change(self.entity_id, self.const.account_type_mod,
  173. None, change_params={'new_pri': int(new_pri),
  174. 'old_pri': int(orig_pri)})
  175.  
  176. def del_account_type(self, ou_id, affiliation):
  177. cols = {'person_id': self.owner_id,
  178. 'ou_id': ou_id,
  179. 'affiliation': int(affiliation),
  180. 'account_id': self.entity_id}
  181. where = ' AND '.join(('{} = :{}'.format(x, x) for x in cols.keys()))
  182. priority = self.query_1(
  183. """SELECT priority FROM [:table schema=cerebrum name=account_type]
  184. WHERE {}""".format(where), cols)
  185. self.execute("""
  186. DELETE FROM [:table schema=cerebrum name=account_type]
  187. WHERE {}""".format(where), cols)
  188. self._db.log_change(self.entity_id, self.const.account_type_del,
  189. None,
  190. change_params={'ou_id': int(ou_id),
  191. 'affiliation': int(affiliation),
  192. 'priority': int(priority)})
  193.  
  194. def delete_ac_types(self):
  195. """Delete all the AccountTypes for the account."""
  196.  
  197. self.execute("""
  198. DELETE FROM [:table schema=cerebrum name=account_type]
  199. WHERE account_id=:a_id""", {'a_id': self.entity_id})
  200.  
  201. def list_accounts_by_type(self, ou_id=None, affiliation=None,
  202. status=None, filter_expired=True,
  203. account_id=None, person_id=None,
  204. primary_only=False, person_spread=None,
  205. account_spread=None, fetchall=True,
  206. exclude_account_id=None):
  207. """Return information about the matching accounts.
  208.  
  209. TODO: Add rest of the parameters.
  210.  
  211. @type filter_expired: bool
  212. @param filter_expired:
  213. If accounts marked as expired should be filtered away from the
  214. output.
  215.  
  216. @type primary_only: bool
  217. @param primary_only:
  218. If only the primary account for each person should be returned.
  219.  
  220. @param exclude_account_id: Filter out account(s) with given account_id.
  221. @type exclude_account_id: Integer, list, tuple, set
  222.  
  223.  
  224. @rtype: db-rows
  225. @return: Each row is an account type, containing the person_id, ou_id,
  226. affiliation, account_id and priority.
  227. """
  228. binds = {'ou_id': ou_id,
  229. 'status': status,
  230. 'account_id': account_id}
  231. join = extra = ""
  232. if affiliation is not None:
  233. if isinstance(affiliation, (list, tuple)):
  234. affiliation = "IN (%s)" % \
  235. ", ".join(map(str, map(int, affiliation)))
  236. else:
  237. affiliation = "= %d" % int(affiliation)
  238. extra += " AND at.affiliation %s" % affiliation
  239. if status is not None:
  240. extra += " AND pas.status=:status"
  241. if ou_id is not None:
  242. extra += " AND at.ou_id=:ou_id"
  243. if person_id is not None:
  244. extra += " AND " + argument_to_sql(person_id,
  245. "at.person_id",
  246. binds,
  247. int)
  248. if filter_expired:
  249. extra += " AND (ai.expire_date IS NULL OR ai.expire_date > [:now])"
  250. if account_id:
  251. extra += " AND ai.account_id=:account_id"
  252. if person_spread is not None and account_spread is not None:
  253. raise Errors.CerebrumError('Illegal to use both person and '
  254. 'account spread in query')
  255. if person_spread is not None:
  256. if isinstance(person_spread, (list, tuple)):
  257. person_spread = "IN (%s)" % \
  258. ", ".join(map(str, map(int, person_spread)))
  259. else:
  260. person_spread = "= %d" % int(person_spread)
  261. join += " JOIN [:table schema=cerebrum name=entity_spread] es" \
  262. " ON es.entity_id = at.person_id" \
  263. " AND es.spread " + person_spread
  264. if account_spread is not None:
  265. if isinstance(account_spread, (list, tuple)):
  266. account_spread = "IN (%s)" % \
  267. ", ".join(map(str, map(int, account_spread)))
  268. else:
  269. account_spread = "= %d" % int(account_spread)
  270. join += " JOIN [:table schema=cerebrum name=entity_spread] es" \
  271. " ON es.entity_id = at.account_id" \
  272. " AND es.spread " + account_spread
  273. if exclude_account_id is not None and len(exclude_account_id):
  274. extra += " AND NOT " + argument_to_sql(exclude_account_id,
  275. "ai.account_id",
  276. binds,
  277. int)
  278. rows = self.query("""
  279. SELECT DISTINCT at.person_id, at.ou_id, at.affiliation, at.account_id,
  280. at.priority
  281. FROM [:table schema=cerebrum name=person_affiliation_source] pas,
  282. [:table schema=cerebrum name=account_info] ai,
  283. [:table schema=cerebrum name=account_type] at
  284. %s
  285. WHERE at.person_id=pas.person_id AND
  286. at.ou_id=pas.ou_id AND
  287. at.affiliation=pas.affiliation AND
  288. ai.account_id=at.account_id
  289. %s
  290. ORDER BY at.person_id, at.priority""" % (join, extra),
  291. binds, fetchall=fetchall)
  292. if primary_only:
  293. ret = []
  294. prev = None
  295. for row in rows:
  296. person_id = int(row['person_id'])
  297. if person_id != prev:
  298. ret.append(row)
  299. prev = person_id
  300. return ret
  301. return rows
  302. # end list_accounts_by_type
  303.  
  304.  
  305. class AccountHome(object):
  306. """AccountHome keeps track of where the user's home directory is.
  307.  
  308. A different home dir for each defined home spread may exist.
  309. A home is identified either by a disk_id, or by the string
  310. represented by home.
  311.  
  312. Whenever a users account_home or homedir is modified, we changelog
  313. the new path of the users homedirectory as a string. For
  314. convenience, we also log this path when the entry is deleted.
  315. """
  316.  
  317. def resolve_homedir(self, account_name=None, disk_id=None,
  318. disk_path=None, home=None, spread=None):
  319. """Constructs and returns the users homedir-path. Subclasses
  320. should override with spread spesific behaviour."""
  321. path_separator = '/'
  322. if not account_name:
  323. account_name = self.account_name
  324. if disk_id:
  325. disk = Disk.Disk(self._db)
  326. disk.find(disk_id)
  327. disk_path = disk.path
  328. if home:
  329. if not disk_path:
  330. return home
  331. return disk_path + path_separator + home
  332. if not disk_path:
  333. return None
  334. return disk_path + path_separator + account_name
  335.  
  336. def delete(self):
  337. """Removes all homedirs for an account"""
  338.  
  339. # TODO: This should either call its super class, which should rather be
  340. # Account or Entity, or it should be renamed to delete_home, to avoid
  341. # breaking the mro.
  342.  
  343. self.execute("""
  344. DELETE FROM [:table schema=cerebrum name=account_home]
  345. WHERE account_id=:a_id""", {'a_id': self.entity_id})
  346. self.execute("""
  347. DELETE FROM [:table schema=cerebrum name=homedir]
  348. WHERE account_id=:a_id""", {'a_id': self.entity_id})
  349.  
  350. def clear_home(self, spread):
  351. """Clears home for a spread. Removes homedir if no other
  352. home uses it."""
  353. try:
  354. ah = self.get_home(spread)
  355. except Errors.NotFoundError:
  356. return
  357. old_home = self.resolve_homedir(disk_id=ah['disk_id'],
  358. home=ah['home'],
  359. spread=spread)
  360. self.execute("""
  361. DELETE FROM [:table schema=cerebrum name=account_home]
  362. WHERE account_id=:account_id AND spread=:spread""", {
  363. 'account_id': self.entity_id,
  364. 'spread': int(spread)})
  365. self._db.log_change(
  366. self.entity_id, self.const.account_home_removed, None,
  367. change_params={'spread': int(spread),
  368. 'home': old_home,
  369. 'homedir_id': ah['homedir_id']})
  370.  
  371. # If no other account_home.homedir_id points to this
  372. # homedir.homedir_id, remove it to avoid dangling unused data
  373. count = self.query_1("""SELECT count(*) AS count
  374. FROM [:table schema=cerebrum name=account_home]
  375. WHERE homedir_id=:homedir_id""", {'homedir_id': ah['homedir_id']})
  376. if count < 1:
  377. self._clear_homedir(ah['homedir_id'])
  378.  
  379. def set_homedir(self, current_id=NotSet, home=NotSet, disk_id=NotSet,
  380. status=NotSet):
  381. """Adds or updates an entry in the homedir table.
  382.  
  383. If current_id=NotSet, insert a new entry. Otherwise update
  384. the values != NotSet for the given homedir_id=current_id.
  385.  
  386. Returns the homedir_id for the affetcted row.
  387.  
  388. Whenever current_id=NotSet, the caller should follow-up by
  389. calling set_home on the returned homedir_id to assert that we
  390. do not end up with homedir rows without corresponding
  391. account_home entries.
  392. """
  393. binds = {'account_id': self.entity_id,
  394. 'home': home,
  395. 'disk_id': disk_id,
  396. 'status': status
  397. }
  398.  
  399. if current_id is NotSet:
  400. # Allocate new id
  401. binds['homedir_id'] = long(self.nextval('homedir_id_seq'))
  402.  
  403. # Specify None as default value for create
  404. for key, value in binds.items():
  405. if value is NotSet:
  406. binds[key] = None
  407.  
  408. sql = """
  409. INSERT INTO [:table schema=cerebrum name=homedir]
  410. (%s)
  411. VALUES (%s)""" % (
  412. ", ".join(binds.keys()),
  413. ", ".join([":%s" % t for t in binds]))
  414.  
  415. change_type = self.const.homedir_add
  416. else:
  417. # Leave previous value alone if update
  418. for key, value in binds.items():
  419. if value is NotSet:
  420. del binds[key]
  421.  
  422. binds['homedir_id'] = current_id
  423.  
  424. sql = """
  425. UPDATE [:table schema=cerebrum name=homedir]
  426. SET %s
  427. WHERE homedir_id=:homedir_id""" % (
  428. ", ".join(["%s=:%s" % (t, t) for t in binds]))
  429.  
  430. change_type = self.const.homedir_update
  431.  
  432. self.execute(sql, binds)
  433.  
  434. if binds.get('disk_id') or binds.get('home'):
  435. tmp = {'home': self.resolve_homedir(disk_id=binds.get('disk_id'),
  436. home=binds.get('home'))}
  437. else:
  438. tmp = {}
  439. tmp['homedir_id'] = binds['homedir_id']
  440. if 'status' in binds:
  441. tmp['status'] = binds['status']
  442. self._db.log_change(self.entity_id,
  443. change_type,
  444. None,
  445. change_params=tmp)
  446.  
  447. return binds['homedir_id']
  448.  
  449. def _clear_homedir(self, homedir_id):
  450. """Called from clear_home. Removes actual homedir."""
  451. tmp = self.get_homedir(homedir_id)
  452. tmp = self.resolve_homedir(disk_id=tmp['disk_id'],
  453. home=tmp['home'])
  454. self.execute("""
  455. DELETE FROM [:table schema=cerebrum name=homedir]
  456. WHERE homedir_id=:homedir_id""",
  457. {'homedir_id': homedir_id})
  458. self._db.log_change(
  459. self.entity_id, self.const.homedir_remove, None,
  460. change_params={'homedir_id': homedir_id,
  461. 'home': tmp})
  462.  
  463. def get_homedir(self, homedir_id):
  464. return self.query_1("""
  465. SELECT homedir_id, account_id, home, disk_id, status
  466. FROM [:table schema=cerebrum name=homedir]
  467. WHERE homedir_id=:homedir_id""",
  468. {'homedir_id': homedir_id})
  469.  
  470. def get_homepath(self, spread):
  471. tmp = self.get_home(spread)
  472. return self.resolve_homedir(disk_id=tmp["disk_id"], home=tmp["home"])
  473.  
  474. def set_home(self, spread, homedir_id):
  475. """Set the accounts account_home to point to the given
  476. homedir_id for the given spread.
  477. """
  478. binds = {'account_id': self.entity_id,
  479. 'spread': int(spread),
  480. 'homedir_id': homedir_id
  481. }
  482. tmp = self.get_homedir(homedir_id)
  483. tmp = self.resolve_homedir(disk_id=tmp['disk_id'],
  484. home=tmp['home'],
  485. spread=spread)
  486. try:
  487. old = self.get_home(spread)
  488. self.execute("""
  489. UPDATE [:table schema=cerebrum name=account_home]
  490. SET homedir_id=:homedir_id
  491. WHERE account_id=:account_id AND spread=:spread""", binds)
  492. self._db.log_change(
  493. self.entity_id, self.const.account_home_updated, None,
  494. change_params={
  495. 'spread': int(spread),
  496. 'home': tmp,
  497. 'homedir_id': homedir_id
  498. })
  499. # Remove old homedir entry if no account_home points to it anymore
  500. if int(old['homedir_id']) not in [
  501. int(row['homedir_id']) for row in self.get_homes()]:
  502. self._clear_homedir(old['homedir_id'])
  503. except Errors.NotFoundError:
  504. self.execute("""
  505. INSERT INTO [:table schema=cerebrum name=account_home]
  506. (account_id, spread, homedir_id)
  507. VALUES
  508. (:account_id, :spread, :homedir_id)""", binds)
  509. self._db.log_change(
  510. self.entity_id, self.const.account_home_added, None,
  511. change_params={'spread': int(spread),
  512. 'home': tmp,
  513. 'homedir_id': homedir_id})
  514.  
  515. def get_home(self, spread):
  516. return self.query_1("""
  517. SELECT ah.homedir_id, disk_id, home, status, spread
  518. FROM [:table schema=cerebrum name=account_home] ah,
  519. [:table schema=cerebrum name=homedir] ahd
  520. WHERE ah.homedir_id=ahd.homedir_id AND ah.account_id=:account_id
  521. AND spread=:spread""",
  522. {'account_id': self.entity_id,
  523. 'spread': int(spread)})
  524.  
  525. def get_homes(self):
  526. """Return a list of all the given account's registered homes."""
  527. return self.query("""
  528. SELECT ah.homedir_id, ahd.disk_id, ahd.home, ahd.status, ah.spread
  529. FROM [:table schema=cerebrum name=account_home] ah,
  530. [:table schema=cerebrum name=homedir] ahd
  531. WHERE ah.homedir_id=ahd.homedir_id AND ah.account_id=:account_id""",
  532. {'account_id': self.entity_id})
  533.  
  534.  
  535. Entity_class = Utils.Factory.get("Entity")
  536.  
  537.  
  538. @six.python_2_unicode_compatible
  539. class Account(AccountType, AccountHome, EntityName, EntityQuarantine,
  540. EntityExternalId, EntityContactInfo, EntitySpread, Entity_class):
  541.  
  542. __read_attr__ = ('__in_db', '__plaintext_password', 'created_at'
  543. # TODO: Get rid of these.
  544. )
  545. __write_attr__ = ('account_name', 'owner_type', 'owner_id',
  546. 'np_type', 'creator_id', 'expire_date', 'description',
  547. '_auth_info', '_acc_affect_auth_types')
  548.  
  549. def deactivate(self):
  550. """Deactivate is commonly thought of as removal of spreads and setting
  551. of expire_date < today. in addition a deactivated account should not
  552. have any group memberships."""
  553. group = Utils.Factory.get("Group")(self._db)
  554. self.expire_date = mx.DateTime.now()
  555. for s in self.get_spread():
  556. self.delete_spread(int(s['spread']))
  557. for row in group.search(member_id=self.entity_id):
  558. group.clear()
  559. group.find(row['group_id'])
  560. group.remove_member(self.entity_id)
  561. group.write_db()
  562. self.write_db()
  563.  
  564. def delete(self):
  565. """Really, really remove the account, homedir, account types and the
  566. password history."""
  567.  
  568. if self.__in_db:
  569. # Homedir needs to be removed first
  570. AccountHome.delete(self)
  571.  
  572. # Remove the account types
  573. self.delete_ac_types()
  574.  
  575. self.execute("""
  576. DELETE FROM [:table schema=cerebrum name=account_authentication]
  577. WHERE account_id=:a_id""", {'a_id': self.entity_id})
  578. self.execute("""
  579. DELETE FROM [:table schema=cerebrum name=account_info]
  580. WHERE account_id=:a_id""", {'a_id': self.entity_id})
  581.  
  582. # Remove name of account from the account namespace.
  583. self.delete_entity_name(self.const.account_namespace)
  584. self._db.log_change(
  585. self.entity_id,
  586. self.const.account_destroy,
  587. None)
  588.  
  589. # AccountHome is the class "breaking" the MRO-delete() "chain".
  590. # self.__super.delete()
  591. # ... call will stop right at AccountHome. I.e. EntityName, etc won't
  592. # have their respective delete()s called with __super/super() in this
  593. # particular case.
  594. super(AccountHome, self).delete()
  595.  
  596. def terminate(self):
  597. """Deletes the account and all data related to it. The different
  598. instances should subclass it and feed it with what they need to delete
  599. before the account could be fully removed from the database.
  600.  
  601. Note that also change_log entries are removed, so you don't get any log
  602. entry when executing this method. However, change_log events where the
  603. given entity_id is in L{change_by} is not removed, as the API does not
  604. know what to do with such events, as they are for other entities. If
  605. such events occur, a db constraint will complain.
  606.  
  607. It works by first removing related data, before it runs L{delete},
  608. which deletes the account itself.
  609. """
  610. if not self.entity_id:
  611. raise RuntimeError('No account set')
  612. e_id = self.entity_id
  613.  
  614. # Deactivating the account first. This is not an optimal solution, but
  615. # it solves race conditions like for update_email_addresses, which
  616. # recreates EmailTarget and e-mail addresses for the account until it
  617. # is deactivated. Makes termination a bit slower.
  618. self.deactivate()
  619.  
  620. # Have to write latest change_log entries, to be able to remove them:
  621. self._db.write_log()
  622. for row in self._db.get_log_events(any_entity=e_id):
  623. # TODO: any_entity or just subject_entity?
  624. self._db.remove_log_event(int(row['change_id']))
  625.  
  626. # Add this if we put it in Entity as well:
  627. # self.__super.terminate()
  628. self.delete()
  629. self.clear()
  630.  
  631. # Remove the log changes from the deletion too:
  632. self._db.write_log()
  633. for row in self._db.get_log_events(any_entity=e_id):
  634. # TODO: any_entity or just subject_entity?
  635. self._db.remove_log_event(int(row['change_id']))
  636.  
  637. def clear(self):
  638. super(Account, self).clear()
  639. self.clear_class(Account)
  640. self.__updated = []
  641.  
  642. # TODO: The following attributes are currently not in
  643. # Account.__slots__, which means they will stop working
  644. # once all Entity classes have been ported to use the
  645. # mark_update metaclass.
  646. self._auth_info = {}
  647. self._acc_affect_auth_types = []
  648.  
  649. def __eq__(self, other):
  650. assert isinstance(other, Account)
  651.  
  652. if (
  653. self.account_name != other.account_name or
  654. int(self.owner_type) != int(other.owner_type) or
  655. self.owner_id != other.owner_id or
  656. self.np_type != other.np_type or
  657. self.creator_id != other.creator_id or
  658. self.expire_date != other.expire_date or
  659. self.description != other.description
  660. ):
  661. return False
  662. return True
  663.  
  664. def populate(self, name, owner_type, owner_id, np_type, creator_id,
  665. expire_date, description=None, parent=None):
  666. if parent is not None:
  667. self.__xerox__(parent)
  668. else:
  669. Entity_class.populate(self, self.const.entity_account)
  670. # If __in_db is present, it must be True; calling populate on
  671. # an object where __in_db is present and False is very likely
  672. # a programming error.
  673. #
  674. # If __in_db in not present, we'll set it to False.
  675. try:
  676. if not self.__in_db:
  677. raise RuntimeError("populate() called multiple times.")
  678. except AttributeError:
  679. self.__in_db = False
  680. self.owner_type = int(owner_type)
  681. self.owner_id = owner_id
  682. self.np_type = np_type
  683. self.creator_id = creator_id
  684. self.expire_date = expire_date
  685. self.description = description
  686. self.account_name = name
  687.  
  688. def affect_auth_types(self, *authtypes):
  689. self._acc_affect_auth_types = list(authtypes)
  690.  
  691. def populate_authentication_type(self, type, value):
  692. self._auth_info[int(type)] = value
  693. self.__updated.append('password')
  694.  
  695. def wants_auth_type(self, method):
  696. """Returns True if this authentication type should be stored
  697. for this account."""
  698. return True
  699.  
  700. def set_password(self, plaintext):
  701. """Updates all account_authentication entries with an
  702. encrypted version of the plaintext password. The methods to
  703. be used are determined by AUTH_CRYPT_METHODS.
  704.  
  705. Note: affect_auth_types is automatically extended to contain
  706. these methods.
  707. """
  708. notimplemented = []
  709. for method_name in cereconf.AUTH_CRYPT_METHODS:
  710. method = self.const.Authentication(method_name)
  711. if method not in self._acc_affect_auth_types:
  712. self._acc_affect_auth_types.append(method)
  713. if not self.wants_auth_type(method):
  714. # affect_auth_types is set above, so existing entries
  715. # which are unwanted for this account will be removed.
  716. #
  717. # HOWEVER, removing a method from AUTH_CRYPT_METHODS
  718. # will not cause deletion of the associated auth_data
  719. # upon next password change, the auth_data for that
  720. # method will stick around as stale data.
  721. #
  722. # So to stop storing a method, you'll either have to
  723. # clean it out from the database manually, or you'll
  724. # have to decline it in wants_auth_data until it's all
  725. # gone.
  726. continue
  727. try:
  728. enc = self.encrypt_password(method, plaintext)
  729. except Errors.NotImplementedAuthTypeError as e:
  730. notimplemented.append(str(e))
  731. else:
  732. self.populate_authentication_type(method, enc)
  733. try:
  734. # Allow multiple writes, even though this is a __read_attr__
  735. del self.__plaintext_password
  736. except AttributeError:
  737. pass
  738. finally:
  739. self.__plaintext_password = plaintext
  740.  
  741. if notimplemented:
  742. raise Errors.NotImplementedAuthTypeError("\n".join(notimplemented))
  743.  
  744. def encrypt_password(self, method, plaintext, salt=None, binary=False):
  745. """Returns the plaintext hashed according to the specified
  746. method. A mixin for a new method should not call super for
  747. the method it handles.
  748.  
  749. This should be fixed for python3
  750.  
  751. :type method: Constants.AccountAuthentication
  752. :param method: Some auth_type_x constant
  753.  
  754. :type plaintext: String (unicode)
  755. :param plaintext: The plaintext to hash
  756.  
  757. :type salt: String (unicode)
  758. :param salt: Salt for hashing
  759.  
  760. :type binary: bool
  761. :param binary: Treat plaintext as binary data
  762. """
  763. unicode_plaintext = plaintext
  764. if binary:
  765. utf8_plaintext = plaintext # a small lie
  766. else:
  767. assert(isinstance(unicode_plaintext, six.text_type))
  768. utf8_plaintext = unicode_plaintext.encode('utf-8')
  769. if method in (self.const.auth_type_md5_crypt,
  770. self.const.auth_type_crypt3_des,
  771. self.const.auth_type_sha256_crypt,
  772. self.const.auth_type_sha512_crypt,
  773. self.const.auth_type_ssha):
  774. if salt is None:
  775. saltchars = string.ascii_letters + string.digits + "./"
  776. if method == self.const.auth_type_md5_crypt:
  777. salt = "$1$" + Utils.random_string(8, saltchars)
  778. elif method == self.const.auth_type_sha256_crypt:
  779. salt = "$5$" + Utils.random_string(16, saltchars)
  780. elif method == self.const.auth_type_sha512_crypt:
  781. salt = "$6$" + Utils.random_string(16, saltchars)
  782. else:
  783. salt = Utils.random_string(2, saltchars)
  784. if method == self.const.auth_type_ssha:
  785. # encodestring annoyingly adds a '\n' at the end of
  786. # the string, and OpenLDAP won't accept that.
  787. # b64encode does not, but it requires Python 2.4
  788. return base64.encodestring(
  789. hashlib.sha1(
  790. utf8_plaintext + salt.encode('utf-8')
  791. ).digest() + salt.encode('utf-8')).strip().decode()
  792. return crypt.crypt(
  793. plaintext if binary else utf8_plaintext,
  794. salt.encode('utf-8')).decode()
  795. elif method == self.const.auth_type_md4_nt:
  796. # Do the import locally to avoid adding a dependency for
  797. # those who don't want to support this method.
  798. import smbpasswd
  799. return smbpasswd.nthash(unicode_plaintext).decode()
  800. elif method == self.const.auth_type_plaintext:
  801. return unicode_plaintext
  802. elif method == self.const.auth_type_md5_unsalt:
  803. return hashlib.md5(utf8_plaintext).hexdigest().decode()
  804. elif method == self.const.auth_type_ha1_md5:
  805. s = ":".join(
  806. [self.account_name,
  807. cereconf.AUTH_HA1_REALM,
  808. unicode_plaintext])
  809. return hashlib.md5(s.encode('utf-8')).hexdigest().decode()
  810. raise Errors.NotImplementedAuthTypeError(
  811. 'Unknown method {method}'.format(method=method))
  812.  
  813. def decrypt_password(self, method, cryptstring):
  814. """Returns the decrypted plaintext according to the specified
  815. method. If decryption is impossible, NotImplementedError is
  816. raised. A mixin for a new method should not call super for
  817. the method it handles.
  818. """
  819. if method in (self.const.auth_type_md5_crypt,
  820. self.const.auth_type_ha1_md5,
  821. self.const.auth_type_crypt3_des,
  822. self.const.auth_type_sha256_crypt,
  823. self.const.auth_type_sha512_crypt,
  824. self.const.auth_type_md4_nt):
  825. raise NotImplementedError(
  826. "Can't decrypt {method}".format(method=method))
  827. elif method == self.const.auth_type_plaintext:
  828. return cryptstring
  829. raise ValueError('Unknown method {method}'.format(method=method))
  830.  
  831. def verify_password(self, method, plaintext, cryptstring):
  832. """Returns True if the plaintext matches the cryptstring,
  833. False if it doesn't. If the method doesn't support
  834. verification, NotImplemented is returned.
  835. """
  836. if method not in (self.const.auth_type_md5_crypt,
  837. self.const.auth_type_ha1_md5,
  838. self.const.auth_type_crypt3_des,
  839. self.const.auth_type_md4_nt,
  840. self.const.auth_type_ssha,
  841. self.const.auth_type_sha256_crypt,
  842. self.const.auth_type_sha512_crypt,
  843. self.const.auth_type_plaintext):
  844. raise ValueError('Unknown method {method}'.format(method=method))
  845. salt = cryptstring
  846. if method == self.const.auth_type_ssha:
  847. salt = base64.decodestring(
  848. cryptstring.encode())[20:].decode()
  849. return (self.encrypt_password(method,
  850. plaintext,
  851. salt=salt) == cryptstring)
  852.  
  853. def verify_auth(self, plaintext):
  854. """Try to verify all authentication data stored for an
  855. account. Authentication data from methods not listed in
  856. AUTH_CRYPT_METHODS are ignored. Returns True if all stored
  857. authentication methods which are able to confirm a plaintext,
  858. do. If no methods are able to confirm, or one method reports
  859. a mismatch, return False.
  860. """
  861. success = []
  862. failed = []
  863. for m in cereconf.AUTH_CRYPT_METHODS:
  864. method = self.const.Authentication(m)
  865. try:
  866. auth_data = self.get_account_authentication(method)
  867. except Errors.NotFoundError:
  868. # No credentials set by the given method for the given account.
  869. # This is normally okay, as we could create new methods without
  870. # requiring all users to set new passwords. However, at least
  871. # one auth method has to succeed.
  872. continue
  873. status = self.verify_password(method, plaintext, auth_data)
  874. if status is NotImplemented:
  875. continue
  876. if status is True:
  877. success.append(m)
  878. else:
  879. failed.append(m)
  880. # If you both pass and fail various crypt methods, it is normally an
  881. # error:
  882. if success and failed:
  883. if hasattr(self, 'logger'):
  884. self.logger.warn('Cred mismatch for %s, success: %s, fail: %s',
  885. self.account_name, success, failed)
  886.  
  887. if not success and not failed:
  888. if hasattr(self, 'logger'):
  889. self.logger.warn('Nothing to authenticate agaist for %s',
  890. self.account_name)
  891. return False
  892.  
  893. return success and not failed
  894.  
  895. def illegal_name(self, name):
  896. """Return a string with error message if username is illegal"""
  897. return False
  898.  
  899. def write_db(self):
  900. self.__super.write_db()
  901. if not self.__updated:
  902. return
  903. if 'account_name' in self.__updated:
  904. tmp = self.illegal_name(self.account_name)
  905. if tmp:
  906. raise self._db.IntegrityError, "Illegal username: %s" % tmp
  907.  
  908. is_new = not self.__in_db
  909. # make dict of changes to send to changelog
  910. newvalues = {}
  911. for key in self.__updated:
  912. # "password" is not handled by mark_update, so getattr
  913. # won't work. _auth_info and _acc_affect_auth_types
  914. # should not be logged.
  915. # TBD: Why are they in __updated?
  916. if key == 'np_type':
  917. newvalues[key] = int(getattr(self, key))
  918. elif key not in ['_auth_info',
  919. '_acc_affect_auth_types',
  920. 'password']:
  921. newvalues[key] = getattr(self, key)
  922.  
  923. # mark_update will not change the value if the new value is
  924. # __eq__ to the old. in other words, it's impossible to
  925. # convert it from _CerebrumCode-instance to an integer.
  926. if self.np_type is None:
  927. np_type = None
  928. else:
  929. np_type = int(self.np_type)
  930. if is_new:
  931. cols = [('entity_type', ':e_type'),
  932. ('account_id', ':acc_id'),
  933. ('owner_type', ':o_type'),
  934. ('owner_id', ':o_id'),
  935. ('np_type', ':np_type'),
  936. ('creator_id', ':c_id')]
  937. # Columns that have default values through DDL.
  938. if self.expire_date is not None:
  939. cols.append(('expire_date', ':exp_date'))
  940. if self.description is not None:
  941. cols.append(('description', ':desc'))
  942. self.execute("""
  943. INSERT INTO [:table schema=cerebrum name=account_info] (%(tcols)s)
  944. VALUES (%(binds)s)""" % {'tcols': ", ".join([x[0] for x in cols]),
  945. 'binds': ", ".join([x[1] for x in cols])},
  946. {'e_type': int(self.const.entity_account),
  947. 'acc_id': self.entity_id,
  948. 'o_type': int(self.owner_type),
  949. 'c_id': self.creator_id,
  950. 'o_id': self.owner_id,
  951. 'np_type': np_type,
  952. 'exp_date': self.expire_date,
  953. 'desc': self.description})
  954. self._db.log_change(self.entity_id, self.const.account_create,
  955. None, change_params=newvalues)
  956. self.add_entity_name(
  957. self.const.account_namespace,
  958. self.account_name)
  959. else:
  960. cols = [('owner_type', ':o_type'),
  961. ('owner_id', ':o_id'),
  962. ('np_type', ':np_type'),
  963. ('creator_id', ':c_id'),
  964. ('description', ':desc'),
  965. ('expire_date', ':exp_date')]
  966. self.execute("""
  967. UPDATE [:table schema=cerebrum name=account_info]
  968. SET %(defs)s
  969. WHERE account_id=:acc_id""" % {'defs': ", ".join(
  970. ["%s=%s" % x for x in cols])},
  971. {'o_type': int(self.owner_type),
  972. 'c_id': self.creator_id,
  973. 'o_id': self.owner_id,
  974. 'np_type': np_type,
  975. 'exp_date': self.expire_date,
  976. 'desc': self.description,
  977. 'acc_id': self.entity_id})
  978. self._db.log_change(self.entity_id, self.const.account_mod,
  979. None, change_params=newvalues)
  980. if 'account_name' in self.__updated:
  981. self.update_entity_name(self.const.account_namespace,
  982. self.account_name)
  983. # We store the plaintext password in the changelog so that
  984. # other systems that need it may get it. The changelog
  985. # handler should remove the plaintext password using some
  986. # criteria.
  987. try:
  988. password_str = self.__plaintext_password
  989. del password_str
  990. except AttributeError:
  991. # TODO: this is meant to catch that self.__plaintext_password is
  992. # unset, trying to use hasattr() instead will surprise you
  993. pass
  994. else:
  995. # self.__plaintext_password is set. Put the value in the
  996. # changelog if the configuration tells us to.
  997. change_params = None
  998. if cereconf.PASSWORD_PLAINTEXT_IN_CHANGE_LOG:
  999. change_params = {'password': self.__plaintext_password}
  1000. self._db.log_change(self.entity_id,
  1001. self.const.account_password,
  1002. None,
  1003. change_params=change_params)
  1004. # Store the authentication data.
  1005. for k in self._acc_affect_auth_types:
  1006. k = int(k)
  1007. what = 'insert'
  1008. if self.__in_db:
  1009. try:
  1010. dta = self.get_account_authentication(k)
  1011. if dta != self._auth_info.get(k, None):
  1012. what = 'update'
  1013. else:
  1014. what = 'nothing'
  1015. except Errors.NotFoundError:
  1016. # insert
  1017. pass
  1018. if self._auth_info.get(k, None) is not None:
  1019. if what == 'insert':
  1020. self.execute("""
  1021. INSERT INTO
  1022. [:table schema=cerebrum name=account_authentication]
  1023. (account_id, method, auth_data)
  1024. VALUES (:acc_id, :method, :auth_data)""",
  1025. {'acc_id': self.entity_id, 'method': k,
  1026. 'auth_data': self._auth_info[k]})
  1027. elif what == 'update':
  1028. self.execute("""
  1029. UPDATE [:table schema=cerebrum name=account_authentication]
  1030. SET auth_data=:auth_data
  1031. WHERE account_id=:acc_id AND method=:method""",
  1032. {'acc_id': self.entity_id, 'method': k,
  1033. 'auth_data': self._auth_info[k]})
  1034. elif self.__in_db and what == 'update':
  1035. self.execute("""
  1036. DELETE FROM [:table schema=cerebrum name=account_authentication]
  1037. WHERE account_id=:acc_id AND method=:method""",
  1038. {'acc_id': self.entity_id, 'method': k})
  1039.  
  1040. try:
  1041. del self.__plaintext_password
  1042. except AttributeError:
  1043. pass
  1044.  
  1045. del self.__in_db
  1046. self.__in_db = True
  1047. self.__updated = []
  1048. return is_new
  1049.  
  1050. def new(self, name, owner_type, owner_id, np_type, creator_id,
  1051. expire_date, description=None):
  1052. self.populate(name, owner_type, owner_id, np_type, creator_id,
  1053. expire_date, description=description)
  1054. self.write_db()
  1055. self.find(self.entity_id)
  1056.  
  1057. def find(self, account_id):
  1058. self.__super.find(account_id)
  1059.  
  1060. (self.owner_type, self.owner_id,
  1061. self.np_type, self.creator_id,
  1062. self.expire_date, self.description) = self.query_1("""
  1063. SELECT owner_type, owner_id, np_type,
  1064. creator_id, expire_date, description
  1065. FROM [:table schema=cerebrum name=account_info]
  1066. WHERE account_id=:a_id""", {'a_id': account_id})
  1067. self.account_name = self.get_name(self.const.account_namespace)
  1068. try:
  1069. del self.__in_db
  1070. except AttributeError:
  1071. pass
  1072. self.__in_db = True
  1073. self.__updated = []
  1074.  
  1075. def find_by_name(self, name, domain=None):
  1076. if domain is None:
  1077. domain = int(self.const.account_namespace)
  1078. EntityName.find_by_name(self, name, domain)
  1079.  
  1080. def get_account_authentication_methods(self):
  1081. """Return a list of the authentication methods the account has."""
  1082. binds = {'a_id': self.entity_id}
  1083.  
  1084. return self.query("""
  1085. SELECT method
  1086. FROM [:table schema=cerebrum name=account_authentication]
  1087. WHERE account_id=:a_id""", binds)
  1088.  
  1089. def get_account_authentication(self, method):
  1090. """Return the authentication data for the given method. Raise
  1091. an exception if missing."""
  1092.  
  1093. return self.query_1("""
  1094. SELECT auth_data
  1095. FROM [:table schema=cerebrum name=account_authentication]
  1096. WHERE account_id=:a_id AND method=:method""",
  1097. {'a_id': self.entity_id,
  1098. 'method': int(method)})
  1099.  
  1100. def get_account_expired(self):
  1101. """Return expire_date if account expire date is overdue, else False"""
  1102. try:
  1103. return self.query_1("""
  1104. SELECT expire_date
  1105. FROM [:table schema=cerebrum name=account_info]
  1106. WHERE expire_date < [:now] AND account_id=:a_id""",
  1107. {'a_id': self.entity_id})
  1108. except Errors.NotFoundError:
  1109. return False
  1110.  
  1111. # TODO: is_reserved and list_reserved_users belong in an extended
  1112. # version of Account
  1113. def is_reserved(self):
  1114. """We define a reserved account as an account with no
  1115. expire_date and no spreads"""
  1116. if (not self.is_expired()) and (not self.get_spread()):
  1117. return True
  1118. return False
  1119.  
  1120. def is_deleted(self):
  1121. """We define a deleted account as an account with
  1122. expire_date < now() and no spreads"""
  1123. if self.is_expired() and not self.get_spread():
  1124. return True
  1125. return False
  1126.  
  1127. def is_expired(self):
  1128. now = mx.DateTime.now()
  1129. if self.expire_date is None or self.expire_date >= now:
  1130. return False
  1131. return True
  1132.  
  1133. def list(self, filter_expired=True, fetchall=True):
  1134. """Returns all accounts"""
  1135. where = []
  1136. if filter_expired:
  1137. where.append("(ai.expire_date IS NULL OR ai.expire_date > [:now])")
  1138. if where:
  1139. where = "WHERE %s" % " AND ".join(where)
  1140. else:
  1141. where = ""
  1142. return self.query("""
  1143. SELECT *
  1144. FROM [:table schema=cerebrum name=account_info] ai %s""" % where,
  1145. fetchall=fetchall)
  1146.  
  1147. def list_account_home(self, home_spread=None, account_spread=None,
  1148. disk_id=None, host_id=None, include_nohome=False,
  1149. filter_expired=True):
  1150. """List users with homedirectory, optionally filtering the
  1151. results on home/account spread, disk/host.
  1152.  
  1153. If include_nohome=True, users without home will be included in
  1154. the search-result when filtering on home_spread. Should not
  1155. be used in combination with filter on disk/host."""
  1156.  
  1157. where = ['en.entity_id=ai.account_id']
  1158. where.append('ei.entity_id=ai.account_id')
  1159. tables = ['[:table schema=cerebrum name=entity_name] en']
  1160. tables.append(', [:table schema=cerebrum name=entity_info] ei')
  1161. if account_spread is not None:
  1162. # Add this table before account_info for correct left-join syntax
  1163. where.append("es.entity_id=ai.account_id")
  1164. where.append("es.spread=:account_spread")
  1165. tables.append(", [:table schema=cerebrum name=entity_spread] es")
  1166.  
  1167. tables.append(', [:table schema=cerebrum name=account_info] ai')
  1168. if filter_expired:
  1169. where.append("(ai.expire_date IS NULL OR ai.expire_date > [:now])")
  1170.  
  1171. # We must perform a left-join or inner-join depending on
  1172. # whether or not include_nohome is True.
  1173. if include_nohome:
  1174. if home_spread is not None:
  1175. tables.append(
  1176. ('LEFT JOIN [:table schema=cerebrum name=account_home] ah '
  1177. ' ON ah.account_id=ai.account_id AND '
  1178. 'ah.spread=:home_spread'))
  1179. else:
  1180. tables.append(
  1181. 'LEFT JOIN [:table schema=cerebrum name=account_home] ah' +
  1182. ' ON ah.account_id=ai.account_id')
  1183. tables.append(
  1184. ('LEFT JOIN ([:table schema=cerebrum name=homedir] hd '
  1185. 'LEFT JOIN [:table schema=cerebrum name=disk_info] d '
  1186. 'ON d.disk_id = hd.disk_id) '
  1187. 'ON hd.homedir_id=ah.homedir_id'))
  1188. else:
  1189. tables.extend([
  1190. ', [:table schema=cerebrum name=account_home] ah ',
  1191. ', [:table schema=cerebrum name=homedir] hd ' +
  1192. ' LEFT JOIN [:table schema=cerebrum name=disk_info] d ' +
  1193. ' ON d.disk_id = hd.disk_id'])
  1194. where.extend(["ai.account_id=ah.account_id",
  1195. "ah.homedir_id=hd.homedir_id"])
  1196. if home_spread is not None:
  1197. where.append("ah.spread=:home_spread")
  1198.  
  1199. if disk_id is not None:
  1200. where.append("hd.disk_id=:disk_id")
  1201. if host_id is not None:
  1202. where.append("d.host_id=:host_id")
  1203. where = " AND ".join(where)
  1204. tables = "\n".join(tables)
  1205.  
  1206. return self.query("""
  1207. SELECT ai.account_id, en.entity_name, hd.home,
  1208. ah.spread AS home_spread, d.path, hd.homedir_id, ai.owner_id,
  1209. hd.status, ai.expire_date, ai.description, ei.created_at,
  1210. d.disk_id, d.host_id
  1211. FROM %s
  1212. WHERE %s""" % (tables, where), {
  1213. 'home_spread': int(home_spread or 0),
  1214. 'account_spread': int(account_spread or 0),
  1215. 'disk_id': disk_id,
  1216. 'host_id': host_id
  1217. })
  1218.  
  1219. def list_reserved_users(self, fetchall=True):
  1220. """Return all reserved users"""
  1221. return self.query("""
  1222. SELECT *
  1223. FROM [:table schema=cerebrum name=account_info] ai
  1224. WHERE ai.expire_date IS NULL AND NOT EXISTS (
  1225. SELECT 'foo' FROM [:table schema=cerebrum name=entity_spread] es
  1226. WHERE es.entity_id=ai.account_id)""",
  1227. fetchall=fetchall)
  1228.  
  1229. def list_deleted_users(self):
  1230. """Return all deleted users"""
  1231. return self.query("""
  1232. SELECT *
  1233. FROM [:table schema=cerebrum name=account_info] ai
  1234. WHERE ai.expire_date < [:now] AND NOT EXISTS (
  1235. SELECT 'foo' FROM [:table schema=cerebrum name=entity_spread] es
  1236. WHERE es.entity_id=ai.account_id)""")
  1237.  
  1238. def list_accounts_by_owner_id(self, owner_id, owner_type=None,
  1239. filter_expired=True):
  1240. """Return a list of account-ids, or None if none found."""
  1241. if owner_type is None:
  1242. owner_type = self.const.entity_person
  1243. where = "owner_id = :o_id AND owner_type = :o_type"
  1244. if filter_expired:
  1245. where += " AND (expire_date IS NULL OR expire_date > [:now])"
  1246. return self.query("""
  1247. SELECT account_id
  1248. FROM [:table schema=cerebrum name=account_info]
  1249. WHERE """ + where, {'o_id': int(owner_id),
  1250. 'o_type': int(owner_type)})
  1251.  
  1252. def list_account_authentication(self, auth_type=None, filter_expired=True,
  1253. account_id=None, spread=None):
  1254. binds = dict()
  1255. tables = []
  1256. where = []
  1257. if auth_type is None:
  1258. auth_type = self.const.auth_type_md5_crypt
  1259. aa_method = argument_to_sql(auth_type, 'aa.method', binds, int)
  1260. if spread is not None:
  1261. tables.append('[:table schema=cerebrum name=entity_spread] es')
  1262. where.append('ai.account_id=es.entity_id')
  1263. where.append(argument_to_sql(spread, 'es.spread', binds, int))
  1264. where.append(argument_to_sql(self.const.entity_account,
  1265. 'es.entity_type', binds, int))
  1266. if filter_expired:
  1267. where.append("(ai.expire_date IS NULL OR ai.expire_date > [:now])")
  1268. if account_id:
  1269. where.append(argument_to_sql(account_id, 'ai.account_id',
  1270. binds, int))
  1271. where.append('ai.account_id=en.entity_id')
  1272. where = " AND ".join(where)
  1273. if tables:
  1274. tables = ','.join(tables) + ','
  1275. else:
  1276. tables = ''
  1277. return self.query("""
  1278. SELECT ai.account_id, en.entity_name, aa.method, aa.auth_data
  1279. FROM %s
  1280. [:table schema=cerebrum name=entity_name] en,
  1281. [:table schema=cerebrum name=account_info] ai
  1282. LEFT JOIN [:table schema=cerebrum name=account_authentication] aa
  1283. ON ai.account_id=aa.account_id AND %s
  1284. WHERE %s""" % (tables, aa_method, where), binds)
  1285.  
  1286. def get_account_name(self):
  1287. return self.account_name
  1288.  
  1289. def make_passwd(self, uname, phrase=False, checkers=None):
  1290. """Generate a random password"""
  1291. password_generator = PasswordGenerator()
  1292. for attempt in range(10):
  1293. # try with 10 random passwords before giving up
  1294. if phrase:
  1295. r = password_generator.generate_dictionary_passphrase()
  1296. else:
  1297. r = password_generator.generate_password()
  1298. try:
  1299. check_password(r, self, checkers=checkers)
  1300. return r
  1301. except PasswordNotGoodEnough as e:
  1302. if attempt == 9: # last attempt
  1303. # raise PasswordNotGoodEnough(
  1304. # '(after 10 attempts) ' + str(e))
  1305.  
  1306. # Keep the old behaviour and let the caller handle the bad
  1307. # password.
  1308. # Should not happen unless the configured password rules
  1309. # are too restrictive or min_length > MAKE_PASSWORD_LENGTH
  1310. return r # give up and return the last password
  1311. continue # make a new attempt
  1312.  
  1313. def suggest_unames(self, domain, fname, lname, maxlen=8, suffix=""):
  1314. """Returns a tuple with 15 (unused) username suggestions based
  1315. on the person's first and last name.
  1316.  
  1317. domain: value domain code
  1318. fname: first name (and any middle names)
  1319. lname: last name
  1320. maxlen: maximum length of a username (incl. the suffix)
  1321. suffix: string to append to every generated username
  1322. """
  1323. validate_func = functools.partial(self.validate_new_uname,
  1324. self.const.account_namespace)
  1325. return suggest_usernames(domain, fname, lname,
  1326. maxlen=maxlen, suffix=suffix,
  1327. validate_func=validate_func)
  1328.  
  1329. def validate_new_uname(self, domain, uname):
  1330. """Check that the requested username is legal and free"""
  1331. try:
  1332. # We instantiate EntityName directly because find_by_name
  1333. # calls self.find() whose result may depend on the class
  1334. # of self
  1335. en = EntityName(self._db)
  1336. en.find_by_name(uname, domain)
  1337. return False
  1338. except Errors.NotFoundError:
  1339. return True
  1340.  
  1341. def search(self,
  1342. spread=None,
  1343. name=None,
  1344. owner_id=None,
  1345. owner_type=None,
  1346. expire_start='[:now]',
  1347. expire_stop=None,
  1348. exclude_account_id=None):
  1349. """Retrieves a list of Accounts filtered by the given criterias.
  1350. If no criteria is given, all non-expired accounts are returned.
  1351.  
  1352. If expire_start and expire_stop is used, accounts with expire_date
  1353. between expire_start and expire_stop is returned.
  1354.  
  1355. @param spread: Return entities that has this spread
  1356. @type spread: Either be integer or string. A string with wildcards *
  1357. and ? are expanded for "any chars" and "one char".
  1358.  
  1359. @param name: Return only entities that matches name
  1360. @type name: String. Wildcards * and ? are expanded for "any chars" and
  1361. "one char".
  1362.  
  1363. @param owner_id: Return entities that is owned by the owner_id(s)
  1364. @type owner_id: Integer, list, tuple, set
  1365.  
  1366. @param owner_type: Return entities where owners type is of owner_type
  1367. @type owner_type: Integer
  1368.  
  1369. @param expire_start: Filter on expire_date. If not specified use
  1370. current time. If specified then filter on expire_date>=expire_start.
  1371. If expire_start is None, don't apply a start_date filter.
  1372. @type expire_start: Date. Either a string on format 'YYYY-mm-dd' or a
  1373. mx.DateTime object
  1374.  
  1375. @param expire_stop: Filter on expire_date. If None, don't apply a
  1376. stop filter on expire_date. If other than None, filter on
  1377. expire_date<expire_stop.
  1378. @type expire_stop: Date. Either a string on format 'YYYY-mm-dd' or a
  1379. mx.DateTime object
  1380.  
  1381. @param exclude_account_id: Filter out account(s) with given account_id.
  1382. @type exclude_account_id: Integer, list, tuple, set
  1383.  
  1384. @return a list of tuples with the info (account_id,name,owner_id,
  1385. owner_type,expire_date).
  1386. """
  1387.  
  1388. tables = []
  1389. where = []
  1390. tables.append("[:table schema=cerebrum name=account_info] ai")
  1391. tables.append("[:table schema=cerebrum name=entity_name] en")
  1392. where.append("en.entity_id=ai.account_id")
  1393. where.append("en.value_domain=:vdomain")
  1394. binds = {"vdomain": int(self.const.account_namespace)}
  1395.  
  1396. if spread is not None:
  1397. tables.append("[:table schema=cerebrum name=entity_spread] es")
  1398. where.append("ai.account_id=es.entity_id")
  1399. where.append("es.entity_type=:entity_type")
  1400. try:
  1401. spread = int(spread)
  1402. except (TypeError, ValueError):
  1403. spread = prepare_string(spread)
  1404. tables.append("[:table schema=cerebrum name=spread_code] sc")
  1405. where.append("es.spread=sc.code")
  1406. where.append("LOWER(sc.code_str) LIKE :spread")
  1407. else:
  1408. where.append("es.spread=:spread")
  1409. binds['spread'] = spread
  1410. binds['entity_type'] = int(self.const.entity_account)
  1411.  
  1412. if name is not None:
  1413. name = prepare_string(name)
  1414. where.append("LOWER(en.entity_name) LIKE :name")
  1415. binds['name'] = name
  1416.  
  1417. if owner_id is not None:
  1418. where.append(argument_to_sql(owner_id, "ai.owner_id", binds, int))
  1419.  
  1420. if owner_type is not None:
  1421. where.append("ai.owner_type=:owner_type")
  1422. binds['owner_type'] = owner_type
  1423.  
  1424. if expire_start and expire_stop:
  1425. where.append(
  1426. ("(ai.expire_date>=:expire_start "
  1427. "and ai.expire_date<:expire_stop)"))
  1428. binds['expire_start'] = expire_start
  1429. binds['expire_stop'] = expire_stop
  1430. elif expire_start and expire_stop is None:
  1431. where.append(
  1432. "(ai.expire_date>=:expire_start or ai.expire_date IS NULL)")
  1433. binds['expire_start'] = expire_start
  1434. elif expire_start is None and expire_stop:
  1435. where.append("ai.expire_date<:expire_stop")
  1436. binds['expire_stop'] = expire_stop
  1437.  
  1438. if exclude_account_id is not None and len(exclude_account_id):
  1439. where.append("NOT " + argument_to_sql(exclude_account_id,
  1440. "ai.account_id",
  1441. binds,
  1442. int))
  1443. where_str = ""
  1444. if where:
  1445. where_str = "WHERE " + " AND ".join(where)
  1446.  
  1447. return self.query("""
  1448. SELECT DISTINCT ai.account_id AS account_id, en.entity_name AS name,
  1449. ai.owner_id AS owner_id, ai.owner_type AS owner_type,
  1450. ai.expire_date AS expire_date, ai.description AS
  1451. description,
  1452. ai.np_type AS np_type
  1453. FROM %s %s""" % (','.join(tables), where_str), binds)
  1454.  
  1455. def __str__(self):
  1456. if hasattr(self, 'account_name'):
  1457. return self.account_name
  1458. else:
  1459. return u'<unbound account>'
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement