forgot to check in
[plcapi.git] / PLC / Persons.py
1 #
2 # Functions for interacting with the persons table in the database
3 #
4
5 from datetime import datetime
6 from types import StringTypes
7 try:
8     from hashlib import md5
9 except ImportError:
10     from md5 import md5
11 import time
12 from random import Random
13 import re
14 import crypt
15
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
25
26 class Person(AlchemyObj):
27     """
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().
31     """
32
33     tablename = 'persons'
34
35     fields = {
36         'person_id': Parameter(int, "User identifier", primary_key=True),
37         'keystone_id': Parameter(str, "Keystone User identifier"),
38         'first_name': Parameter(str, "Given name", max = 128),
39         'last_name': Parameter(str, "Surname", max = 128),
40         'title': Parameter(str, "Title", max = 128, nullok = True),
41         'email': Parameter(str, "Primary e-mail address", max = 254),
42         'phone': Parameter(str, "Telephone number", max = 64, nullok = True),
43         'url': Parameter(str, "Home page", max = 254, nullok = True),
44         'bio': Parameter(str, "Biography", max = 254, nullok = True),
45         'enabled': Parameter(bool, "Has been enabled"),
46         'password': Parameter(str, "Account password in crypt() form", max = 254),
47         'verification_key': Parameter(str, "Reset password key", max = 254, nullok = True),
48         'verification_expires': Parameter(datetime, "Date and time when verification_key expires", nullok = True),
49         'last_updated': Parameter(datetime, "Date and time of last update", ro = True, nullok=True),
50         'date_created': Parameter(datetime, "Date and time when account was created", ro = True, default=datetime.now()),
51         'role_ids': Parameter([int], "List of role identifiers", joined=True),
52         'roles': Parameter([str], "List of roles", joined=True),
53         'site_ids': Parameter([int], "List of site identifiers", joined=True),
54         'key_ids': Parameter([int], "List of key identifiers", joined=True),
55         'slice_ids': Parameter([int], "List of slice identifiers", joined=True),
56         'peer_id': Parameter(int, "Peer to which this user belongs", nullok = True),
57         'peer_person_id': Parameter(int, "Foreign user identifier at peer", nullok = True),
58         'person_tag_ids' : Parameter ([int], "List of tags attached to this person", joined=True),
59         }
60
61     def validate_last_updated(self, last_updated):
62         # always return current timestamp
63         last_updated = datetime.now()
64         return last_updated
65
66     def validate_email(self, email):
67         """
68         Validate email address. Stolen from Mailman.
69         """
70         email = email.lower()
71         invalid_email = PLCInvalidArgument("Invalid e-mail address")
72
73         if not email:
74             raise invalid_email
75
76         email_re = re.compile('\A[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9._\-]+\.[a-zA-Z]+\Z')
77         if not email_re.match(email):
78             raise invalid_email
79
80         return email
81
82     def can_update(self, person):
83         """
84         Returns true if we can update the specified person. We can
85         update a person if:
86
87         1. We are the person.
88         2. We are an admin.
89         3. We are a PI and the person is a user or tech or at
90            one of our sites.
91         """
92
93         assert isinstance(person, Person)
94
95         if self.person_id == person.person_id:
96             return True
97
98         if 'admin' in self['roles']:
99             return True
100
101         if 'pi' in self['roles']:
102             if set(self['site_ids']).intersection(person['site_ids']):
103                 # non-admin users cannot update a person who is neither a PI or ADMIN
104                 return (not set(['pi','admin']).intersection(person['roles']))
105
106         return False
107
108     def can_view(self, person):
109         """
110         Returns true if we can view the specified person. We can
111         view a person if:
112
113         1. We are the person.
114         2. We are an admin.
115         3. We are a PI or Tech and the person is at one of our sites.
116         """
117
118         assert isinstance(person, Person)
119
120         if self.can_update(person):
121             return True
122
123         # pis and techs can see all people on their site
124         if set(['pi','tech']).intersection(self['roles']):
125             if set(self['site_ids']).intersection(person['site_ids']):
126                 return True
127
128         return False
129
130     def add_role(self, role_name, site_filter = {}):
131         assert 'keystone_id' in self
132         from PLC.Sites import Sites
133         user = self.api.client_shell.keystone.users.find(id=self['keystone_id'])
134         roles = Roles(self.api, {'name': role_name})
135         if not roles:
136             raise PLCInvalidArgument, "Role %s not found" % role_name 
137         role = roles[0]
138       
139         if site_filter:
140             sites = Sites(self.api, site_filter)
141             for site in sites: 
142                 # add role at the requested site
143                 tenant = self.api.client_shell.keystone.tenants.find(id=site['tenant_id']) 
144                 self.api.client_shell.keystone.roles.add_user_role(user, tenant, role.object)
145         else:
146             # add role to at all of users sites
147             if not self['site_ids']:
148                 raise PLCInvalidArgument, "Cannot add role unless user already belongs to a site or a valid site is specified"
149             for site_id in self['site_ids']:
150                 sites = Sites(self.api, {'site_id': site_id})
151                 site = sites[0]
152                 tenant = self.api.client_shell.keystone.tenants.find(id=site['tenant_id'])
153                 self.api.client_shell.keystone.roles.add_user_role(user, tenant, role.object)
154     
155     def remove_role(self, role_name, login_base=None):
156         assert 'keystone_id' in self
157         user = self.api.client_shell.keystone.users.find(id=self['keystone_id'])
158         roles = Roles(self.api, {'name': role_name})
159         if not roles:
160             raise PLCInvalidArgument, "Role %s not found" % role_name
161         role = roles[0]
162
163         if login_base:
164             # add role at the requested site
165             tenant = self.api.client_shell.keystone.tenants.find(name=login_base)
166             self.api.client_shell.keystone.roles.add_user_role(user, role.object, tenant)
167         else:
168             from PLC.Sites import Sites
169             # add role to at all of users sites
170             if not self['site_ids']:
171                 raise PLCInvalidArgument, "Must specify a valid site or add user to site first"
172             for site_id in self['site_ids']:
173                 sites = Sites(self.api, {'site_id': site_id})
174                 site = sites[0]
175                 tenant = self.api.client_shell.keystone.tenants.find(id=site['tenant_id'])
176                 self.api.client_shell.keystone.roles.remove_user_role(user, role.object, tenant)
177
178     #add_key = Row.add_object(Key, 'person_key')
179     #remove_key = Row.remove_object(Key, 'person_key')
180
181     def sync(self, commit=True, validate=True):
182         assert 'email' in self
183         AlchemyObj.sync(self, commit=commit, validate=validate)
184         # filter out fields that are not supported in keystone
185         nova_fields = ['enabled', 'email', 'password']
186         nova_can_update = lambda (field, value): field in nova_fields
187         nova_person = dict(filter(nova_can_update, self.items()))
188         nova_person['name'] = "%s %s" % (self.get('first_name', ''), self.get('last_name', '')) 
189         if 'person_id' not in self:
190             # check if keystone record exists
191             users = self.api.client_shell.keystone.users.findall(email=self['email'])
192             if not users:
193                 self.object = self.api.client_shell.keystone.users.create(**nova_person)
194             else:
195                 self.object = users[0]
196             self['keystone_id'] = self.object.id
197             AlchemyObj.insert(self, dict(self))
198         else:
199             self.object = self.api.client_shell.keystone.users.update(self['person_id'], nova_person)
200             AlchemyObj.update(self, {'person_id': self['person_id']}, dict(self))
201         
202
203     def delete(self):
204         assert 'person_id' in self
205         assert 'keystone_id' in self
206
207         # delete keystone record
208         nova_user = self.api.client_shell.keystone.users.find(id=self['keystone_id'])
209         self.api.client_shell.keystone.users.delete(nova_user)
210
211         # delete relationships
212         SlicePerson().delete.filter({'person_id': self['person_id']})
213
214         # delete person
215         AlchemyObj.delete(self, dict(self))
216
217  
218 class Persons(list):
219     """
220     Representation of row(s) from the persons table in the
221     database.
222     """
223
224     def __init__(self, api, person_filter = None, columns = None):
225         from PLC.Sites import Sites
226         self.api = api
227         if not person_filter:
228             #persons = self.api.client_shell.keystone.users.findall()
229             persons = Person().select()
230         elif isinstance(person_filter, (list, tuple, set)):
231             ints = filter(lambda x: isinstance(x, (int, long)), person_filter)
232             strs = filter(lambda x: isinstance(x, StringTypes), person_filter)
233             person_filter = {'person_id': ints, 'email': strs}
234             persons = Person().select(filter=person_filter)
235         elif isinstance(person_filter, dict):
236             persons = Person().select(filter=person_filter)
237             #persons = self.api.client_shell.keystone.users.findall(**person_filter)
238         elif isinstance (person_filter, StringTypes):
239             persons = Person().select(filter={'email': person_filter})
240         else:
241             raise PLCInvalidArgument, "Wrong person filter %r"%person_filter
242
243         for person in persons:
244             person = Person(self.api, object=person) 
245             keystone_user = self.api.client_shell.keystone.users.find(id=person['keystone_id'])
246             if not columns or 'site_ids' in columns:
247                 site_persons = SitePerson().select(filter={'person_id': person['person_id']})
248                 person['site_ids'] = [rec.site_id for rec in site_persons]
249             if not columns or 'roles' in columns:
250                 roles = set()
251                 sites = Sites(self.api, person['site_ids'])
252                 for site in sites:
253                     tenant = self.api.client_shell.keystone.tenants.find(id=site['tenant_id'])
254                     tenant_roles = self.api.client_shell.keystone.roles.roles_for_user(keystone_user, tenant)
255                     for tenant_role in tenant_roles:
256                         roles.add(tenant_role.name)  
257                 person['roles'] = list(roles)
258             if not columns or 'key_ids' in columns:
259                 person['key_ids'] = [] 
260             if not columns or 'slice_ids' in columns:
261                 person_slices = SlicePerson().select(filter={'person_id': person['person_id']})
262                 person['slice_ids'] = [rec.slice_id for rec in person_slices]
263             self.append(person)