2 # Functions for interacting with the persons table in the database
5 from datetime import datetime
6 from types import StringTypes
8 from hashlib import md5
12 from random import Random
16 from PLC.Faults import *
17 from PLC.Debug import log
18 from PLC.Parameter import Parameter, Mixed
19 from PLC.Messages import Message, Messages
20 from PLC.Roles import Role, Roles
21 from PLC.Keys import Key, Keys
22 from PLC.SitePersons import SitePerson, SitePersons
23 from PLC.SlicePersons import SlicePerson, SlicePersons
24 from PLC.Storage.AlchemyObject import AlchemyObj
26 class Person(AlchemyObj):
28 Representation of a row in the persons table. To use, optionally
29 instantiate with a dict of values. Update as you would a
30 dict. Commit to the database with sync().
37 'person_id': Parameter(int, "User identifier", primary_key=True),
38 'keystone_id': Parameter(str, "Keystone User identifier"),
39 'first_name': Parameter(str, "Given name", max = 128),
40 'last_name': Parameter(str, "Surname", max = 128),
41 'title': Parameter(str, "Title", max = 128, nullok = True),
42 'email': Parameter(str, "Primary e-mail address", max = 254),
43 'phone': Parameter(str, "Telephone number", max = 64, nullok = True),
44 'url': Parameter(str, "Home page", max = 254, nullok = True),
45 'bio': Parameter(str, "Biography", max = 254, nullok = True),
46 'enabled': Parameter(bool, "Has been enabled"),
47 'password': Parameter(str, "Account password in crypt() form", max = 254),
48 'verification_key': Parameter(str, "Reset password key", max = 254, nullok = True),
49 'verification_expires': Parameter(datetime, "Date and time when verification_key expires", nullok = True),
50 'last_updated': Parameter(datetime, "Date and time of last update", ro = True, nullok=True),
51 'date_created': Parameter(datetime, "Date and time when account was created", ro = True, default=datetime.now()),
52 'role_ids': Parameter([int], "List of role identifiers", joined=True),
53 'roles': Parameter([str], "List of roles", joined=True),
54 'site_ids': Parameter([int], "List of site identifiers", joined=True),
55 'key_ids': Parameter([int], "List of key identifiers", joined=True),
56 'slice_ids': Parameter([int], "List of slice identifiers", joined=True),
57 'peer_id': Parameter(int, "Peer to which this user belongs", nullok = True),
58 'peer_person_id': Parameter(int, "Foreign user identifier at peer", nullok = True),
59 'person_tag_ids' : Parameter ([int], "List of tags attached to this person", joined=True),
62 def validate_last_updated(self, last_updated):
63 # always return current timestamp
64 last_updated = datetime.now()
67 def validate_email(self, email):
69 Validate email address. Stolen from Mailman.
72 invalid_email = PLCInvalidArgument("Invalid e-mail address")
77 email_re = re.compile('\A[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9._\-]+\.[a-zA-Z]+\Z')
78 if not email_re.match(email):
81 # check only against users on the same peer
83 namespace_peer_id = self['peer_id']
85 namespace_peer_id = None
87 conflicts = Person().select(filter={'email':email,'peer_id':namespace_peer_id})
89 for person in conflicts:
90 if 'person_id' not in self or self['person_id'] != person.person_id:
91 raise PLCInvalidArgument, "E-mail address already in use"
95 def validate_password(self, password):
97 Encrypt password if necessary before committing to the
103 if len(password) > len(magic) and \
104 password[0:len(magic)] == magic:
107 # Generate a somewhat unique 8 character salt string
108 salt = str(time.time()) + str(Random().random())
109 salt = md5(salt).hexdigest()[:8]
110 return crypt.crypt(password.encode(self.api.encoding), magic + salt + "$")
112 def can_update(self, person):
114 Returns true if we can update the specified person. We can
117 1. We are the person.
119 3. We are a PI and the person is a user or tech or at
123 assert isinstance(person, Person)
125 if self['person_id'] == person['person_id']:
128 if 'admin' in self['roles']:
131 if 'pi' in self['roles']:
132 if set(self['site_ids']).intersection(person['site_ids']):
133 # non-admin users cannot update a person who is neither a PI or ADMIN
134 return (not set(['pi','admin']).intersection(person['roles']))
138 def can_view(self, person):
140 Returns true if we can view the specified person. We can
143 1. We are the person.
145 3. We are a PI or Tech and the person is at one of our sites.
148 assert isinstance(person, Person)
150 if self.can_update(person):
153 # pis and techs can see all people on their site
154 if set(['pi','tech']).intersection(self['roles']):
155 if set(self['site_ids']).intersection(person['site_ids']):
160 def add_role(self, role_name, site_filter = {}):
161 assert 'keystone_id' in self
162 from PLC.Sites import Sites
163 user = self.api.client_shell.keystone.users.find(id=self['keystone_id'])
164 roles = Roles(self.api, {'name': role_name})
166 raise PLCInvalidArgument, "Role %s not found" % role_name
170 sites = Sites(self.api, site_filter)
172 # add role at the requested site
173 tenant = self.api.client_shell.keystone.tenants.find(id=site['tenant_id'])
174 self.api.client_shell.keystone.roles.add_user_role(user, tenant, role.object)
176 # add role to at all of users sites
177 if not self['site_ids']:
178 raise PLCInvalidArgument, "Cannot add role unless user already belongs to a site or a valid site is specified"
179 for site_id in self['site_ids']:
180 sites = Sites(self.api, {'site_id': site_id})
182 tenant = self.api.client_shell.keystone.tenants.find(id=site['tenant_id'])
183 self.api.client_shell.keystone.roles.add_user_role(user, tenant, role.object)
185 def remove_role(self, role_name, login_base=None):
186 assert 'keystone_id' in self
187 user = self.api.client_shell.keystone.users.find(id=self['keystone_id'])
188 roles = Roles(self.api, {'name': role_name})
190 raise PLCInvalidArgument, "Role %s not found" % role_name
194 # add role at the requested site
195 tenant = self.api.client_shell.keystone.tenants.find(name=login_base)
196 self.api.client_shell.keystone.roles.add_user_role(user, role.object, tenant)
198 from PLC.Sites import Sites
199 # add role to at all of users sites
200 if not self['site_ids']:
201 raise PLCInvalidArgument, "Must specify a valid site or add user to site first"
202 for site_id in self['site_ids']:
203 sites = Sites(self.api, {'site_id': site_id})
205 tenant = self.api.client_shell.keystone.tenants.find(id=site['tenant_id'])
206 self.api.client_shell.keystone.roles.remove_user_role(user, role.object, tenant)
208 #add_key = Row.add_object(Key, 'person_key')
209 #remove_key = Row.remove_object(Key, 'person_key')
211 def sync(self, commit=True, validate=True):
212 assert 'email' in self
213 AlchemyObj.sync(self, commit=commit, validate=validate)
214 # filter out fields that are not supported in keystone
215 nova_fields = ['enabled', 'email', 'password']
216 nova_can_update = lambda (field, value): field in nova_fields
217 nova_person = dict(filter(nova_can_update, self.items()))
218 nova_person['name'] = "%s %s" % (self.get('first_name', ''), self.get('last_name', ''))
219 if 'person_id' not in self:
220 # check if keystone record exists
221 users = self.api.client_shell.keystone.users.findall(email=self['email'])
223 self.object = self.api.client_shell.keystone.users.create(**nova_person)
225 self.object = users[0]
226 self['keystone_id'] = self.object.id
227 AlchemyObj.insert(self, dict(self))
228 person = AlchemyObj.select(self, filter={'keystone_id': self['keystone_id']})[0]
229 self['person_id'] = person.person_id
231 self.object = self.api.client_shell.keystone.users.update(self['person_id'], nova_person)
232 AlchemyObj.update(self, {'person_id': self['person_id']}, dict(self))
236 assert 'person_id' in self
237 assert 'keystone_id' in self
239 # delete keystone record
240 nova_user = self.api.client_shell.keystone.users.find(id=self['keystone_id'])
241 self.api.client_shell.keystone.users.delete(nova_user)
243 # delete relationships
244 for slice_person in SlicePerson().select(filter={'person_id': self['person_id']}):
245 slice_person.delete()
248 AlchemyObj.delete(self, filter={'person_id': self['person_id']})
253 Representation of row(s) from the persons table in the
257 def __init__(self, api, person_filter = None, columns = None):
258 from PLC.Sites import Sites
260 if not person_filter:
261 #persons = self.api.client_shell.keystone.users.findall()
262 persons = Person().select()
263 elif isinstance(person_filter, (list, tuple, set)):
264 ints = filter(lambda x: isinstance(x, (int, long)), person_filter)
265 strs = filter(lambda x: isinstance(x, StringTypes), person_filter)
266 person_filter = {'person_id': ints, 'email': strs}
267 persons = Person().select(filter=person_filter)
268 elif isinstance(person_filter, dict):
269 persons = Person().select(filter=person_filter)
270 #persons = self.api.client_shell.keystone.users.findall(**person_filter)
271 elif isinstance (person_filter, StringTypes):
272 persons = Person().select(filter={'email': person_filter})
274 raise PLCInvalidArgument, "Wrong person filter %r"%person_filter
276 for person in persons:
277 person = Person(self.api, object=person, columns=columns)
278 keystone_user = self.api.client_shell.keystone.users.find(id=person['keystone_id'])
279 if not columns or 'site_ids' in columns:
280 site_persons = SitePerson().select(filter={'person_id': person['person_id']})
281 person['site_ids'] = [rec.site_id for rec in site_persons]
282 if not columns or 'roles' in columns:
284 sites = Sites(self.api, person['site_ids'])
286 tenant = self.api.client_shell.keystone.tenants.find(id=site['tenant_id'])
287 tenant_roles = self.api.client_shell.keystone.roles.roles_for_user(keystone_user, tenant)
288 for tenant_role in tenant_roles:
289 roles.add(tenant_role.name)
290 person['roles'] = list(roles)
291 if not columns or 'key_ids' in columns:
292 person['key_ids'] = []
293 if not columns or 'slice_ids' in columns:
294 person_slices = SlicePerson().select(filter={'person_id': person['person_id']})
295 person['slice_ids'] = [rec.slice_id for rec in person_slices]