# # Functions for interacting with the persons table in the database # from datetime import datetime from types import StringTypes try: from hashlib import md5 except ImportError: from md5 import md5 import time from random import Random import re import crypt from PLC.Faults import * from PLC.Debug import log from PLC.Parameter import Parameter, Mixed from PLC.Messages import Message, Messages from PLC.Roles import Role, Roles from PLC.Keys import Key, Keys from PLC.SitePersons import SitePerson, SitePersons from PLC.SlicePersons import SlicePerson, SlicePersons from PLC.Storage.AlchemyObject import AlchemyObj class Person(AlchemyObj): """ Representation of a row in the persons table. To use, optionally instantiate with a dict of values. Update as you would a dict. Commit to the database with sync(). """ tablename = 'persons' self.refresh(api) fields = { 'person_id': Parameter(int, "User identifier", primary_key=True), 'keystone_id': Parameter(str, "Keystone User identifier"), 'first_name': Parameter(str, "Given name", max = 128), 'last_name': Parameter(str, "Surname", max = 128), 'title': Parameter(str, "Title", max = 128, nullok = True), 'email': Parameter(str, "Primary e-mail address", max = 254), 'phone': Parameter(str, "Telephone number", max = 64, nullok = True), 'url': Parameter(str, "Home page", max = 254, nullok = True), 'bio': Parameter(str, "Biography", max = 254, nullok = True), 'enabled': Parameter(bool, "Has been enabled"), 'password': Parameter(str, "Account password in crypt() form", max = 254), 'verification_key': Parameter(str, "Reset password key", max = 254, nullok = True), 'verification_expires': Parameter(datetime, "Date and time when verification_key expires", nullok = True), 'last_updated': Parameter(datetime, "Date and time of last update", ro = True, nullok=True), 'date_created': Parameter(datetime, "Date and time when account was created", ro = True, default=datetime.now()), 'role_ids': Parameter([int], "List of role identifiers", joined=True), 'roles': Parameter([str], "List of roles", joined=True), 'site_ids': Parameter([int], "List of site identifiers", joined=True), 'key_ids': Parameter([int], "List of key identifiers", joined=True), 'slice_ids': Parameter([int], "List of slice identifiers", joined=True), 'peer_id': Parameter(int, "Peer to which this user belongs", nullok = True), 'peer_person_id': Parameter(int, "Foreign user identifier at peer", nullok = True), 'person_tag_ids' : Parameter ([int], "List of tags attached to this person", joined=True), } def validate_last_updated(self, last_updated): # always return current timestamp last_updated = datetime.now() return last_updated def validate_email(self, email): """ Validate email address. Stolen from Mailman. """ email = email.lower() invalid_email = PLCInvalidArgument("Invalid e-mail address") if not email: raise invalid_email email_re = re.compile('\A[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9._\-]+\.[a-zA-Z]+\Z') if not email_re.match(email): raise invalid_email # check only against users on the same peer if 'peer_id' in self: namespace_peer_id = self['peer_id'] else: namespace_peer_id = None conflicts = Person().select(filter={'email':email,'peer_id':namespace_peer_id}) for person in conflicts: if 'person_id' not in self or self['person_id'] != person.person_id: raise PLCInvalidArgument, "E-mail address already in use" return email def validate_password(self, password): """ Encrypt password if necessary before committing to the database. """ magic = "$1$" if len(password) > len(magic) and \ password[0:len(magic)] == magic: return password else: # Generate a somewhat unique 8 character salt string salt = str(time.time()) + str(Random().random()) salt = md5(salt).hexdigest()[:8] return crypt.crypt(password.encode(self.api.encoding), magic + salt + "$") def can_update(self, person): """ Returns true if we can update the specified person. We can update a person if: 1. We are the person. 2. We are an admin. 3. We are a PI and the person is a user or tech or at one of our sites. """ assert isinstance(person, Person) if self['person_id'] == person['person_id']: return True if 'admin' in self['roles']: return True if 'pi' in self['roles']: if set(self['site_ids']).intersection(person['site_ids']): # non-admin users cannot update a person who is neither a PI or ADMIN return (not set(['pi','admin']).intersection(person['roles'])) return False def can_view(self, person): """ Returns true if we can view the specified person. We can view a person if: 1. We are the person. 2. We are an admin. 3. We are a PI or Tech and the person is at one of our sites. """ assert isinstance(person, Person) if self.can_update(person): return True # pis and techs can see all people on their site if set(['pi','tech']).intersection(self['roles']): if set(self['site_ids']).intersection(person['site_ids']): return True return False def add_role(self, role_name, site_filter = {}): assert 'keystone_id' in self from PLC.Sites import Sites user = self.api.client_shell.keystone.users.find(id=self['keystone_id']) roles = Roles(self.api, {'name': role_name}) if not roles: raise PLCInvalidArgument, "Role %s not found" % role_name role = roles[0] if site_filter: sites = Sites(self.api, site_filter) for site in sites: # add role at the requested site tenant = self.api.client_shell.keystone.tenants.find(id=site['tenant_id']) self.api.client_shell.keystone.roles.add_user_role(user, tenant, role.object) else: # add role to at all of users sites if not self['site_ids']: raise PLCInvalidArgument, "Cannot add role unless user already belongs to a site or a valid site is specified" for site_id in self['site_ids']: sites = Sites(self.api, {'site_id': site_id}) site = sites[0] tenant = self.api.client_shell.keystone.tenants.find(id=site['tenant_id']) self.api.client_shell.keystone.roles.add_user_role(user, tenant, role.object) def remove_role(self, role_name, login_base=None): assert 'keystone_id' in self user = self.api.client_shell.keystone.users.find(id=self['keystone_id']) roles = Roles(self.api, {'name': role_name}) if not roles: raise PLCInvalidArgument, "Role %s not found" % role_name role = roles[0] if login_base: # add role at the requested site tenant = self.api.client_shell.keystone.tenants.find(name=login_base) self.api.client_shell.keystone.roles.add_user_role(user, role.object, tenant) else: from PLC.Sites import Sites # add role to at all of users sites if not self['site_ids']: raise PLCInvalidArgument, "Must specify a valid site or add user to site first" for site_id in self['site_ids']: sites = Sites(self.api, {'site_id': site_id}) site = sites[0] tenant = self.api.client_shell.keystone.tenants.find(id=site['tenant_id']) self.api.client_shell.keystone.roles.remove_user_role(user, role.object, tenant) #add_key = Row.add_object(Key, 'person_key') #remove_key = Row.remove_object(Key, 'person_key') def sync(self, commit=True, validate=True): assert 'email' in self AlchemyObj.sync(self, commit=commit, validate=validate) # filter out fields that are not supported in keystone nova_fields = ['enabled', 'email', 'password'] nova_can_update = lambda (field, value): field in nova_fields nova_person = dict(filter(nova_can_update, self.items())) nova_person['name'] = "%s %s" % (self.get('first_name', ''), self.get('last_name', '')) if 'person_id' not in self: # check if keystone record exists users = self.api.client_shell.keystone.users.findall(email=self['email']) if not users: self.object = self.api.client_shell.keystone.users.create(**nova_person) else: self.object = users[0] self['keystone_id'] = self.object.id AlchemyObj.insert(self, dict(self)) person = AlchemyObj.select(self, filter={'keystone_id': self['keystone_id']})[0] self['person_id'] = person.person_id else: self.object = self.api.client_shell.keystone.users.update(self['person_id'], nova_person) AlchemyObj.update(self, {'person_id': self['person_id']}, dict(self)) def delete(self): assert 'person_id' in self assert 'keystone_id' in self # delete keystone record nova_user = self.api.client_shell.keystone.users.find(id=self['keystone_id']) self.api.client_shell.keystone.users.delete(nova_user) # delete relationships for slice_person in SlicePerson().select(filter={'person_id': self['person_id']}): slice_person.delete() # delete person AlchemyObj.delete(self, filter={'person_id': self['person_id']}) class Persons(list): """ Representation of row(s) from the persons table in the database. """ def __init__(self, api, person_filter = None, columns = None): from PLC.Sites import Sites self.api = api if not person_filter: #persons = self.api.client_shell.keystone.users.findall() persons = Person().select() elif isinstance(person_filter, (list, tuple, set)): ints = filter(lambda x: isinstance(x, (int, long)), person_filter) strs = filter(lambda x: isinstance(x, StringTypes), person_filter) person_filter = {'person_id': ints, 'email': strs} persons = Person().select(filter=person_filter) elif isinstance(person_filter, dict): persons = Person().select(filter=person_filter) #persons = self.api.client_shell.keystone.users.findall(**person_filter) elif isinstance (person_filter, StringTypes): persons = Person().select(filter={'email': person_filter}) else: raise PLCInvalidArgument, "Wrong person filter %r"%person_filter for person in persons: person = Person(self.api, object=person, columns=columns) keystone_user = self.api.client_shell.keystone.users.find(id=person['keystone_id']) if not columns or 'site_ids' in columns: site_persons = SitePerson().select(filter={'person_id': person['person_id']}) person['site_ids'] = [rec.site_id for rec in site_persons] if not columns or 'roles' in columns: roles = set() sites = Sites(self.api, person['site_ids']) for site in sites: tenant = self.api.client_shell.keystone.tenants.find(id=site['tenant_id']) tenant_roles = self.api.client_shell.keystone.roles.roles_for_user(keystone_user, tenant) for tenant_role in tenant_roles: roles.add(tenant_role.name) person['roles'] = list(roles) if not columns or 'key_ids' in columns: person['key_ids'] = [] if not columns or 'slice_ids' in columns: person_slices = SlicePerson().select(filter={'person_id': person['person_id']}) person['slice_ids'] = [rec.slice_id for rec in person_slices] self.append(person)