LDAPLookup.lookup_uniqname()   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
from pyramid.security import authenticated_userid
2
import hashlib
3
import binascii
4
import os
5
from .model import *
6
from . import account
7
from . import event
8
from . import request
9
from . import request_post
10
from chezbetty import utility
11
12
import ldap3
13
import random
14
import string
15
16
class InvalidUserException(Exception):
17
    pass
18
19
20
class LDAPLookup(object):
21
    """class that allows lookup of an individual in the UM directory
22
    based on Michigan ID number, uniqname, MCARD"""
23
24
    SERVER = None
25
    USERNAME = None
26
    BASE_DN = "ou=People,dc=umich,dc=edu"
27
    PASSWORD = None
28
    ATTRIBUTES = ["uid", "entityid", "displayName"]
29
    # http://www.itcs.umich.edu/itcsdocs/r1463/attributes-for-ldap.html
30
    DETAILS_ATTRIBUTES = [
31
            # Could be interesting but require extra perm's from ITS
32
            #"umichInstRoles",
33
            #"umichAlumStatus",
34
            #"umichAAAcadProgram",
35
            #"umichAATermStatus",
36
            #"umichHR",
37
            "notice",
38
            "ou",
39
            "umichDescription",
40
            "umichTitle",
41
            ]
42
43
    def __init__(self):
44
        self.__conn = None
45
46
    def __connect(self):
47
        if not self.__conn:
48
            s = ldap3.Server(self.SERVER, port=636, use_ssl=True, get_info=ldap3.GET_ALL_INFO)
49
            self.__conn = ldap3.Connection(s, auto_bind=True,
50
                    user=self.USERNAME, password=self.PASSWORD,
51
                    client_strategy=ldap3.STRATEGY_SYNC,
52
                    authentication=ldap3.AUTH_SIMPLE
53
            )
54
55
56
    def __do_lookup(self, k, v, attributes, full_dict=False):
57
        self.__connect()
58
        query = "(%s=%s)" % (k, v)
59
        try:
60
            self.__conn.search(self.BASE_DN,
61
                    query,
62
                    ldap3.SEARCH_SCOPE_WHOLE_SUBTREE,
63
                    attributes=attributes
64
            )
65
        except:
66
            # sometimes our connections time out
67
            self.__conn = None
68
            self.__connect()
69
            self.__conn.search(self.BASE_DN,
70
                    query,
71
                    ldap3.SEARCH_SCOPE_WHOLE_SUBTREE,
72
                    attributes=attributes
73
            )
74
75
        if len(self.__conn.response) == 0:
76
            raise InvalidUserException()
77
78
        if full_dict:
79
            return self.__conn.response[0]["attributes"]
80
81
        return {
82
            "umid":self.__conn.response[0]["attributes"]["entityid"],
83
            "uniqname":self.__conn.response[0]["attributes"]["uid"][0],
84
            "name":self.__conn.response[0]["attributes"]["displayName"][0]
85
        }
86
87
    def __lookup(self, k, v):
88
        return self.__do_lookup(k, v, self.ATTRIBUTES)
89
90
    def __detail_lookup(self, k, v):
91
        return self.__do_lookup(k, v,
92
                self.ATTRIBUTES + self.DETAILS_ATTRIBUTES,
93
                full_dict=True,
94
                )
95
96
    def lookup_umid(self, umid, details=False):
97
        if details:
98
            return self.__detail_lookup("entityid", umid)
99
        else:
100
            return self.__lookup("entityid", umid)
101
102
    def lookup_uniqname(self, uniqname, details=False):
103
        if details:
104
            return self.__detail_lookup("uid", uniqname)
105
        else:
106
            return self.__lookup("uid", uniqname)
107
108
109
class User(account.Account):
110
    __tablename__ = 'users'
111
    __mapper_args__ = {'polymorphic_identity': 'user'}
112
113
    id        = Column(Integer, ForeignKey("accounts.id"), primary_key=True)
114
    uniqname  = Column(String(8), nullable=False, unique=True)
115
    umid      = Column(String(8), unique=True)
116
    _password = Column("password", String(255))
117
    _salt     = Column("salt", String(255))
118
    enabled   = Column(Boolean, nullable=False, default=True)
119
    archived  = Column(Boolean, nullable=False, default=False)
120
    role      = Column(Enum("user", "serviceaccount", "manager", "administrator", name="user_type"),
121
                       nullable=False, default="user")
122
123
    administrative_events = relationship(event.Event, foreign_keys=[event.Event.user_id], backref="admin")
124
    events_deleted        = relationship(event.Event, foreign_keys=[event.Event.deleted_user_id], backref="deleted_user")
125
    requests              = relationship(request.Request, foreign_keys=[request.Request.user_id], backref="user")
126
    request_posts         = relationship(
127
                              request_post.RequestPost,
128
                              foreign_keys=[request_post.RequestPost.user_id],
129
                              backref="user",
130
                            )
131
132
    __ldap = LDAPLookup()
133
134
    def __init__(self, uniqname, umid, name):
135
        self.enabled = True
136
        self.uniqname = uniqname
137
        self.umid = umid
138
        self.name = name
139
        self.balance = 0.0
140
141
    def __str__(self):
142
        return "<User: id {}, uniqname {}, umid {}, name {}, balance {}>".\
143
                format(self.id, self.uniqname, self.umid, self.name, self.balance)
144
145
    @classmethod
146
    def from_id(cls, id):
147
        return DBSession.query(cls).filter(cls.id == id).one()
148
149
    def get_details(self):
150
        return self.__ldap.lookup_uniqname(self.uniqname, details=True)
151
152
    @classmethod
153
    def from_uniqname(cls, uniqname, local_only=False):
154
        u = DBSession.query(cls).filter(cls.uniqname == uniqname).first()
155
        if not u and not local_only:
156
            u = cls(**cls.__ldap.lookup_uniqname(uniqname))
157
            DBSession.add(u)
158
        return u
159
160
    @classmethod
161
    def from_umid(cls, umid, create_if_never_seen=False):
162
        u = DBSession.query(cls).filter(cls.umid == umid).first()
163
        if not u:
164
            if create_if_never_seen:
165
                u = cls(**cls.__ldap.lookup_umid(umid))
166
                DBSession.add(u)
167
                utility.new_user_email(u)
168
            else:
169
                raise InvalidUserException()
170
        return u
171
172
    @classmethod
173
    def from_fuzzy(cls, search_str, any=True):
174
        q = DBSession.query(cls)\
175
                     .filter(or_(
176
                            cls.uniqname.ilike('%{}%'.format(search_str)),
177
                            cls.umid.ilike('%{}%'.format(search_str)),
178
                            cls.name.ilike('%{}%'.format(search_str))
179
                      ))
180
        if not any:
181
            q = q.filter(cls.enabled)\
182
                 .filter(cls.archived==False)
183
184
        return q.all()
185
186
    @classmethod
187
    def all(cls):
188
        return DBSession.query(cls)\
189
                        .filter(cls.enabled)\
190
                        .order_by(cls.name)\
191
                        .all()
192
193
    @classmethod
194
    def count(cls):
195
        return DBSession.query(func.count(cls.id).label('c'))\
196
                        .filter(cls.role != 'serviceaccount')\
197
                        .filter(cls.archived == False)\
198
                        .filter(cls.enabled == True)\
199
                        .one().c
200
201
    @classmethod
202
    def get_admins(cls):
203
        return DBSession.query(cls)\
204
                        .filter(cls.enabled)\
205
                        .filter(cls.role=='administrator').all()
206
207
    @classmethod
208
    def get_shame_users(cls):
209
        return DBSession.query(cls)\
210
                        .filter(cls.enabled)\
211
                        .filter(cls.balance < -5)\
212
                        .order_by(cls.balance).all()
213
214
    @classmethod
215
    def get_normal_users(cls):
216
        return DBSession.query(cls)\
217
                        .filter(cls.enabled)\
218
                        .filter(cls.archived == False)\
219
                        .order_by(cls.name).all()
220
221
    @classmethod
222
    def get_archived_users(cls):
223
        return DBSession.query(cls)\
224
                        .filter(cls.enabled)\
225
                        .filter(cls.archived == True)\
226
                        .order_by(cls.name).all()
227
228
    @classmethod
229
    def get_disabled_users(cls):
230
        return DBSession.query(cls)\
231
                        .filter(cls.enabled == False)\
232
                        .order_by(cls.name).all()
233
234
    @classmethod
235
    def get_number_new_users(cls, start=None, end=None):
236
        r = DBSession.query(cls)\
237
                        .filter(cls.enabled == True)
238
239
        if start:
240
            r = r.filter(cls.created_at>=start)
241
        if end:
242
            r = r.filter(cls.created_at<end)
243
244
        return r.count()
245
246
    @classmethod
247
    def get_users_total(cls):
248
        return DBSession.query(func.sum(User.balance).label("total_balance"))\
249
                        .one().total_balance or Decimal(0.0)
250
251
    # Sum the total amount of money in user accounts that we are holding for
252
    # users. This is different from just getting the total because it doesn't
253
    # count users with negative balances
254
    @classmethod
255
    def get_amount_held(cls):
256
        return DBSession.query(func.sum(User.balance).label("total_balance"))\
257
                        .filter(User.balance>0)\
258
                        .one().total_balance or Decimal(0.0)
259
260
    @classmethod
261
    def get_amount_owed(cls):
262
        return DBSession.query(func.sum(User.balance).label("total_balance"))\
263
                        .filter(User.balance<0)\
264
                        .one().total_balance or Decimal(0.0)
265
266
    # Sum the amount of debt that has been moved to archived user
267
    @classmethod
268
    def get_debt_forgiven(cls):
269
        return DBSession.query(func.sum(User.archived_balance).label("total_balance"))\
270
                        .filter(User.archived_balance<0)\
271
                        .filter(User.archived==True)\
272
                        .one().total_balance or Decimal(0.0)
273
274
    # Sum the amount of user balances that we have tentatively absorbed into the
275
    # main betty balance
276
    @classmethod
277
    def get_amount_absorbed(cls):
278
        return DBSession.query(func.sum(User.archived_balance).label("total_balance"))\
279
                        .filter(User.archived_balance>0)\
280
                        .filter(User.archived==True)\
281
                        .one().total_balance or Decimal(0.0)
282
283
    @classmethod
284
    def get_user_count_cumulative(cls):
285
        rows = DBSession.query(cls.created_at)\
286
                        .order_by(cls.created_at)\
287
                        .all()
288
        return utility.timeseries_cumulative(rows)
289
290
    def iterate_recent_items(self, limit=None, allow_duplicates=False, pictures_only=True):
291
        cap_search = 20
292
        items = set()
293
        count = 0
294
        for e in self.events:
295
            if e.type == 'purchase':
296
                for transaction in e.transactions:
297
                    if transaction.type == 'purchase':
298
                        for line_item in transaction.subtransactions:
299
                            cap_search -= 1
300
                            if cap_search == 0:
301
                                return
302
                            if (line_item.item not in items) or allow_duplicates:
303
                                if (line_item.item.img) or not pictures_only:
304
                                    count += 1
305
                                    if limit is not None and count > limit:
306
                                        return
307
                                    yield line_item.item
308
                                    items.add(line_item.item)
309
310
    def __make_salt(self):
311
        return binascii.b2a_base64(open("/dev/urandom", "rb").read(32))[:-3].decode("ascii")
312
313
    @hybrid_property
314
    def password(self):
315
        return self._password
316
317
    @password.setter
318
    def password(self, password):
319
        if password == '':
320
            # Use this to clear the password so the user can't login
321
            self._password = None
322
        else:
323
            self._salt = self.__make_salt()
324
            salted = (self._salt + password).encode('utf-8')
325
            self._password = hashlib.sha256(salted).hexdigest()
326
327
    def random_password(self):
328
        password = ''.join(random.choice(string.ascii_letters + string.digits) for x in range(6))
329
        self._salt = self.__make_salt()
330
        salted = (self._salt + password).encode('utf-8')
331
        self._password = hashlib.sha256(salted).hexdigest()
332
        return password
333
334
    def check_password(self, cand):
335
        if not self._salt:
336
            return False
337
        salted = (self._salt + cand).encode('utf-8')
338
        c = hashlib.sha256(salted).hexdigest()
339
        return c == self._password
340
341
    @property
342
    def has_password(self):
343
        return self._password != None
344
345
    # Cash deposit limit is now fixed at $2 because we have a bill acceptor
346
    @property
347
    def deposit_limit(self):
348
        return 2.0
349
350
351
def get_user(request):
352
    login = authenticated_userid(request)
353
    if not login:
354
        return None
355
    return DBSession.query(User).filter(User.uniqname == login).one()
356
357
358
# This is in a stupid place due to circular input problems
359
@property
360
def __user_from_foreign_key(self):
361
    return User.from_id(self.user_id)
362
event.Event.user = __user_from_foreign_key
363
364
365
def groupfinder(userid, request):
366
    user = User.from_uniqname(userid)
367
    if user.role == "user":
368
        return ["user",]
369
    elif user.role == "manager":
370
        return ["user","manager"]
371
    elif user.role == "administrator":
372
        return ["user","manager","admin","serviceaccount"]
373
    elif user.role == "serviceaccount":
374
        return ["serviceaccount"]
375