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)