added create_network(), delete_network(), create_subnet(), delete_subnet(), process_t...
[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         self.refresh(api)
36     fields = {
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),
60         }
61
62     def validate_last_updated(self, last_updated):
63         # always return current timestamp
64         last_updated = datetime.now()
65         return last_updated
66
67     def validate_email(self, email):
68         """
69         Validate email address. Stolen from Mailman.
70         """
71         email = email.lower()
72         invalid_email = PLCInvalidArgument("Invalid e-mail address")
73
74         if not email:
75             raise invalid_email
76
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):
79             raise invalid_email
80
81         # check only against users on the same peer
82         if 'peer_id' in self:
83             namespace_peer_id = self['peer_id']
84         else:
85             namespace_peer_id = None
86
87         conflicts = Person().select(filter={'email':email,'peer_id':namespace_peer_id})
88
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"  
92         
93         return email
94
95     def validate_password(self, password):
96         """
97         Encrypt password if necessary before committing to the
98         database.
99         """
100
101         magic = "$1$"
102
103         if len(password) > len(magic) and \
104            password[0:len(magic)] == magic:
105             return password
106         else:
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 + "$")
111
112     def can_update(self, person):
113         """
114         Returns true if we can update the specified person. We can
115         update a person if:
116
117         1. We are the person.
118         2. We are an admin.
119         3. We are a PI and the person is a user or tech or at
120            one of our sites.
121         """
122
123         assert isinstance(person, Person)
124
125         if self['person_id'] == person['person_id']:
126             return True
127
128         if 'admin' in self['roles']:
129             return True
130
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']))
135
136         return False
137
138     def can_view(self, person):
139         """
140         Returns true if we can view the specified person. We can
141         view a person if:
142
143         1. We are the person.
144         2. We are an admin.
145         3. We are a PI or Tech and the person is at one of our sites.
146         """
147
148         assert isinstance(person, Person)
149
150         if self.can_update(person):
151             return True
152
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']):
156                 return True
157
158         return False
159
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})
165         if not roles:
166             raise PLCInvalidArgument, "Role %s not found" % role_name 
167         role = roles[0]
168       
169         if site_filter:
170             sites = Sites(self.api, site_filter)
171             for site in sites: 
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)
175         else:
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})
181                 site = sites[0]
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)
184     
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})
189         if not roles:
190             raise PLCInvalidArgument, "Role %s not found" % role_name
191         role = roles[0]
192
193         if login_base:
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)
197         else:
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})
204                 site = sites[0]
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)
207
208     #add_key = Row.add_object(Key, 'person_key')
209     #remove_key = Row.remove_object(Key, 'person_key')
210
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'])
222             if not users:
223                 self.object = self.api.client_shell.keystone.users.create(**nova_person)
224             else:
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
230         else:
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))
233         
234
235     def delete(self):
236         assert 'person_id' in self
237         assert 'keystone_id' in self
238
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)
242
243         # delete relationships
244         for slice_person in SlicePerson().select(filter={'person_id': self['person_id']}):
245             slice_person.delete() 
246
247         # delete person
248         AlchemyObj.delete(self, filter={'person_id':  self['person_id']})
249
250  
251 class Persons(list):
252     """
253     Representation of row(s) from the persons table in the
254     database.
255     """
256
257     def __init__(self, api, person_filter = None, columns = None):
258         from PLC.Sites import Sites
259         self.api = api
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})
273         else:
274             raise PLCInvalidArgument, "Wrong person filter %r"%person_filter
275
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:
283                 roles = set()
284                 sites = Sites(self.api, person['site_ids'])
285                 for site in sites:
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]
296             self.append(person)
297