· 7 years ago · Oct 22, 2018, 04:34 PM
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
21arbitrary type. Extentions like PosixUser are used for additional
22parameters that may be required by the requested backend.
23
24Usernames are stored in the table entity_name. The domain that the
25default username is stored in is yet to be determined.
26"""
27from __future__ import unicode_literals
28
29import crypt
30import functools
31import string
32import mx
33import hashlib
34import base64
35
36import six
37
38from Cerebrum import Utils, Disk
39from Cerebrum.Entity import (EntityName,
40 EntityQuarantine,
41 EntityContactInfo,
42 EntityExternalId,
43 EntitySpread)
44from Cerebrum import Errors
45from Cerebrum.Utils import (NotSet,
46 argument_to_sql,
47 prepare_string)
48from Cerebrum.utils.username import suggest_usernames
49from Cerebrum.modules.pwcheck.checker import (check_password,
50 PasswordNotGoodEnough)
51from Cerebrum.modules.password_generator.generator import PasswordGenerator
52
53import cereconf
54
55
56class 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
305class 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
535Entity_class = Utils.Factory.get("Entity")
536
537
538@six.python_2_unicode_compatible
539class 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>'