1#!/usr/bin/env python3
2
3"""Read from the USERS and AUTH_USER table.
4
5As of Django 1.5 it might be possible to use the Django User objects instead.
6"""
7
8import json
9from datetime import datetime
10from typing import Union
11
12from lxml import etree
13
14from chart import settings
15from chart.common.xml import parsechildbool
16from chart.common.xml import parsechildstr
17from chart.common.xml import parsechildstrs
18from chart.common.xml import parsechildstringlist
19from chart.db.connection import db_connect
20from chart.db.settings import DatabaseEngine
21from chart.common.util import nvl
22from chart.common.xml import xml_to_str
23from chart.common.xml import parse_xml
24from chart.common.xml import Element
25from chart.common.xml import SubElement
26
27db_conn = db_connect() # used for auth_user and users tables
28
29CAN_PUBLISH_REPORTS = 'can-publish-reports'
30ELEM_TABLES = 'tables'
31ELEM_GROUPS = 'groups'
32
33def create_user(username, first_name=None, last_name=None, email=None, db_name=None):
34 """Create a new Django user.
35
36<<<hidden due to potential security issue>>>
37 This function should be integrated into the User class.
38 Is used by web.auth and the users tool.
39 Set `db_name` to a database connection to select a non-default db. This function does not
40 connection set by set_db_name().
41 """
42 # maybe wrong; can trigger "apps aren't loaded yet" error if done at module scope
43 from django.contrib.auth.models import User as DjangoUser
44
45<<<hidden due to potential security issue>>>
46 if first_name is not None:
47 user.first_name = first_name
48
49 if last_name is not None:
50 user.last_name = last_name
51 else:
52 user.last_name = username
53
54 if email is not None:
55 user.email = email
56
57 user.is_staff = True # only Staff can see the admin screen
58 user.is_superuser = False
59 user.last_login = datetime.utcnow()
60 user.save(using=db_name)
61
62 db_conn.query('INSERT INTO USERS (django_user_id, daily_digest, notifications) '
63 'VALUES (:user_id, :daily_digest, :notifications)',
64 user_id=user.id,
65 daily_digest=0, # Oracle has no boolean type so we use int 0/1
66 notifications=0)
67 db_conn.commit()
68
69 return user
70
71
72def delete_user(username, db_name=None):
73 """Delete named user from AUTH_USER and USERS tables."""
74 # maybe wrong; can trigger "apps aren't loaded yet" error if done at module scope
75 from django.contrib.auth.models import User as DjangoUser
76 user = DjangoUser.objects.get(username=username)
77 db_conn.query('DELETE FROM USERS WHERE DJANGO_USER_ID={id}'.format(id=user.id))
78 db_conn.commit()
79 user.delete(using=db_name)
80
81
82def passwd_user(username, passwd, db_name=None):
83<<<hidden due to potential security issue>>>
84 # maybe wrong; can trigger "apps aren't loaded yet" error if done at module scope
85 from django.contrib.auth.models import User as DjangoUser
86 user = DjangoUser.objects.get(username=username)
87<<<hidden due to potential security issue>>>
88 user.save(using=db_name)
89
90
91class NoSuchUser(Exception):
92 """User ID or Name not found in AUTH_USERS table.
93
94 (USERS is for supplemental information only).
95 """
96
97 def __init__(self, user_id=None, user_name=None):
98 super(NoSuchUser, self).__init__()
99 self.user_id = user_id
100 self.user_name = user_name
101
102 def __str__(self):
103 if self.user_id is not None:
104 return 'Cannot find user id {id}'.format(id=self.user_id)
105
106 elif self.user_name is not None:
107 return 'Cannot find user name {name}'.format(name=self.user_name)
108
109 return 'Cannot find user'
110
111
112class User:
113 """Read from the USERS and AUTH_USER table."""
114
115 def __init__(self,
116 user_name_or_id:Union[str,int]=None,
117 user_name:str=None,
118 user_id:int=None,
119 data_access:dict=None):
120 """Args:
121 `user_name_or_id`: Helper parameter to read user from database using either
122 string name or numeric user id
123 `user_name`: Read user by name
124 `user_id`: Read user by ID
125 `data_access`: If creating a new user set their access permissions
126 """
127 if user_name_or_id is not None:
128 if isinstance(user_name_or_id, int):
129 user_id = user_name_or_id
130
131 elif user_name_or_id.isdigit():
132 user_id = int(user_name_or_id)
133
134 else:
135 user_name = user_name_or_id
136
137 if user_id is None:
138 row = db_conn.query('SELECT id FROM auth_user WHERE username=:username',
139 username=user_name).fetchone()
140 if row is None:
141 raise NoSuchUser(user_name=user_name)
142
143 user_id = row[0]
144
145 self.user_id = user_id
146
147 # applicable data access restrictions for user
148 self.data_access = data_access
149
150 def __hash__(self):
151 return self.user_id
152
153 @property
154 def user_name(self):
155 """Return USER_NAME."""
156 return db_conn.query('SELECT username FROM auth_user WHERE id=:id',
157 id=self.user_id).fetchone()[0]
158
159 @property
160 def first_name(self):
161 """Return FIRST_NAME."""
162 return db_conn.query('SELECT first_name FROM auth_user WHERE id=:id',
163 id=self.user_id).fetchone()[0]
164
165 @property
166 def last_name(self):
167 """Return LAST_NAME."""
168 return db_conn.query('SELECT last_name FROM auth_user WHERE id=:id',
169 id=self.user_id).fetchone()[0]
170
171 @property
172 def email(self):
173 """Return EMAIL."""
174 return db_conn.query('SELECT email FROM auth_user WHERE id=:id',
175 id=self.user_id).fetchone()[0]
176
177 @property
178 def last_login(self):
179 """Return LAST_LOGIN."""
180 return db_conn.query('SELECT last_login FROM auth_user WHERE id=:id',
181 id=self.user_id).fetchone()[0]
182
183 @property
184 def date_joined(self):
185 """Return DATE_JOINED."""
186 return db_conn.query('SELECT date_joined FROM auth_user WHERE id=:id',
187 id=self.user_id).fetchone()[0]
188
189 @property
190 def daily_digest(self):
191 """Return LAST_LOGIN."""
192 return db_conn.query('SELECT daily_digest FROM users WHERE djang_user_id=:id',
193 id=self.user_id).fetchone()[0] == 1
194
195 @property
196 def notifications(self):
197 """Return NOTIFICATIONS."""
198 row = db_conn.query('SELECT notifications FROM users WHERE django_user_id=:id',
199 id=self.user_id).fetchone()
200 if row is None:
201 return False
202
203 return row[0] == 1
204
205 @property
206 def write_events(self):
207 """Return WRITE_EVENTS."""
208 row = db_conn.query('SELECT write_events FROM users WHERE django_user_id=:id',
209 id=self.user_id).fetchone()
210 if row is None:
211 return False
212
213 return row[0] == 1
214
215 @property
216 def valid_groups(self):
217 """Return a list of parameter tables and groups valid for the user to access."""
218 return self.define_data_access
219
220
221 @property
222 def name(self):
223 """Return the user full name as a single string."""
224 return ('{first} {last}'.format(
225 first=nvl(self.first_name).strip(),
226 last=nvl(self.last_name).strip())).strip()
227
228
229 @staticmethod
230 def all():
231 """Yield all User objects."""
232 # for row in db_conn.query('SELECT auth_user.id, auth_user.username, auth_user.first_name,'
233 # ' auth_user.last_name, auth_user.email, auth_user.last_login,'
234 # ' auth_user.date_joined, users.daily_digest, users.notifications,'
235 # ' users.write_events, users.properties '
236 # 'FROM auth_user, users '
237 # 'WHERE auth_user.id=users.django_user_id '):
238 # 'AND users.notifications=:notifications',
239 # notifications=notifications):
240 # yield User(row=row)
241
242 for user_id, in db_conn.query('SELECT id FROM auth_user'):
243 yield User(user_id=user_id)
244
245 def check_users_entry(self):
246 """Make sure there is an entry in USERS for us."""
247 cc = db_conn.query('SELECT count(*) FROM users WHERE django_user_id=:id',
248 id=self.user_id).fetchone()
249 if cc[0] == 0:
250 db_conn.query('INSERT INTO USERS (django_user_id, daily_digest, notifications) '
251 'VALUES (:user_id, :daily_digest, :notifications)',
252 user_id=self.user_id,
253 daily_digest=0,
254 notifications=0)
255 db_conn.commit()
256
257 def set_properties(self, properties):
258 """Set the properties XML for the provided user."""
259 self.check_users_entry()
260 db_conn.query('UPDATE users '
261 'SET properties=:properties '
262 'WHERE django_user_id=:id',
263 id=self.user_id, properties=properties)
264 db_conn.commit()
265
266 def get_properties(self):
267 """Get properties XML from provided user."""
268 if db_conn.engine == DatabaseEngine.ORACLE:
269 fieldname = 'u.properties.getBlobVal(871)'
270
271 else:
272 fieldname = 'u.properties'
273
274 row = db_conn.query(
275 'SELECT {field} '
276 'FROM users u '
277 'WHERE django_user_id=:id'.format(field=fieldname),
278 id=self.user_id).fetchone()
279
280 if row is None or row[0] is None:
281 # not everyone has an entry in USERS
282 return None
283
284 if db_conn.engine == DatabaseEngine.ORACLE:
285 xml_props = row[0].read()
286
287 else:
288 xml_props = row[0]
289
290 return parse_xml(xml_props)
291
292 properties = property(get_properties,
293 set_properties)
294
295 def set_can_publish_reports(self, value):
296 """Set the Publishing privileges of a given user."""
297 root_elem = Element('properties')
298 SubElement(root_elem, CAN_PUBLISH_REPORTS).text = 'true' if value else 'false'
299
300 if settings.TABLE_SECURITY_RESTRICTIONS:
301 value = self.define_data_access
302 SubElement(root_elem, ELEM_TABLES).text = str(value['tables'])
303 SubElement(root_elem, ELEM_GROUPS).text = str(value['groups'])
304
305 self.set_properties(xml_to_str(root_elem))
306
307 def get_can_publish_reports(self):
308 """Check whether the user can publish reports."""
309 props = self.properties
310 if props is None:
311 return False
312
313 return parsechildbool(self.properties, CAN_PUBLISH_REPORTS, False)
314
315 can_publish_reports = property(get_can_publish_reports,
316 set_can_publish_reports)
317
318 def set_define_data_access(self, data_access):
319 """Set data access for USER.
320 Example User has acess to:
321 {'tables': ['a', 'b'], 'groups':['p12', 'q23']}
322 i.e. access granted to only groups p12 and q23 within tables a and b.
323 all other data is blocked
324 if none of blocked groups in a specified table all data in that table is accessible
325 if no groups defined all data in defined tables allowed
326 TBD - maybe expanded to allow specific parameters only as well
327 """
328 root_elem = Element('properties')
329
330 value = self.get_can_publish_reports
331 SubElement(root_elem, CAN_PUBLISH_REPORTS).text = 'true' if value else 'false'
332 SubElement(root_elem, ELEM_TABLES).text = str(data_access['tables'])
333 SubElement(root_elem, ELEM_GROUPS).text = str(data_access['groups'])
334 self.set_properties(xml_to_str(root_elem))
335
336 def get_define_data_access(self):
337 """Get data access for USER.
338 Example User has acess to:
339 {'tables': ['a', 'b'], 'groups':['p12', 'q23']}
340 """
341
342 if self.data_access is not None:
343 return self.data_access
344
345 props = self.properties
346
347 if props is None:
348 return False
349
350 tables = parsechildstr(props, ELEM_TABLES, None)
351 groups = parsechildstr(props, ELEM_GROUPS, None)
352
353 if tables is None and groups is None:
354 return False # 'No data access Restrictions'
355
356 # reformat to real dict type
357 access = {'tables': tables, 'groups': groups}
358 s = str(access).replace('"', '').replace("'", '"')
359
360 self.data_access = json.loads(s)
361 return json.loads(s)
362
363 define_data_access = property(get_define_data_access,
364 set_define_data_access)
365
366 @staticmethod
367 def from_django_request(django_request:dict):
368 """Helper function to return a User object if someone is logged in, otherwise None."""
369 if django_request.user.id is None:
370 return None
371
372 return User(user_id=django_request.user.id)