]> git.ipfire.org Git - pbs.git/blame - src/buildservice/users.py
users: Refactor fetching any user data from LDAP
[pbs.git] / src / buildservice / users.py
CommitLineData
857a7836 1#!/usr/bin/python3
9137135a 2
98b826d4 3import datetime
b9c2a52b 4import email.utils
857a7836 5import ldap
9137135a 6import logging
f6e6ff79 7import pytz
857a7836 8import time
9137135a
MT
9
10import tornado.locale
11
2c909128 12from . import base
9137135a 13
b9c2a52b
MT
14from .decorators import *
15
a329017a
MT
16# Setup logging
17log = logging.getLogger("pakfire.builservice.users")
18
857a7836
MT
19# A list of LDAP attributes that we fetch
20LDAP_ATTRS = (
21 # UID
22 "uid",
23
24 # Common Name
25 "cn",
26
27 # First & Last Name
28 "givenName", "sn"
29
30 # Email Addresses
31 "mail",
32 "mailAlternateAddress",
33)
34
9137135a 35class Users(base.Object):
857a7836
MT
36 #def init(self):
37 # self.ldap = ldap.LDAP(self.backend)
38
39 @lazy_property
40 def ldap(self):
41 ldap_uri = self.backend.config.get("ldap", "uri")
42
43 log.debug("Connecting to %s..." % ldap_uri)
44
45 # Establish LDAP connection
46 return ldap.initialize(ldap_uri)
8d8d65b4 47
b9c2a52b
MT
48 def _get_user(self, query, *args):
49 res = self.db.get(query, *args)
9137135a 50
b9c2a52b
MT
51 if res:
52 return User(self.backend, res.id, data=res)
9137135a 53
b9c2a52b
MT
54 def _get_users(self, query, *args):
55 res = self.db.query(query, *args)
8d8d65b4 56
b9c2a52b
MT
57 for row in res:
58 yield User(self.backend, row.id, data=row)
9137135a 59
b9c2a52b 60 def __iter__(self):
857a7836
MT
61 users = self._get_users("""
62 SELECT
63 *
64 FROM
65 users
66 WHERE
67 deleted IS FALSE
68 ORDER BY
69 name
70 """,
71 )
8d8d65b4 72
b9c2a52b 73 return iter(users)
8d8d65b4 74
b9c2a52b 75 def __len__(self):
857a7836
MT
76 res = self.db.get("""
77 SELECT
78 COUNT(*) AS count
79 FROM
80 users
81 WHERE
82 deleted IS FALSE
83 """,
84 )
8d8d65b4 85
b9c2a52b 86 return res.count
9137135a 87
857a7836
MT
88 def _ldap_query(self, query, attrlist=None, limit=0, search_base=None):
89 search_base = self.backend.config.get("ldap", "base")
9137135a 90
857a7836 91 log.debug("Performing LDAP query (%s): %s" % (search_base, query))
9137135a 92
857a7836 93 t = time.time()
9137135a 94
857a7836
MT
95 # Ask for up to 512 results being returned at a time
96 page_control = ldap.controls.SimplePagedResultsControl(True, size=512, cookie="")
9137135a 97
857a7836
MT
98 results = []
99 pages = 0
aede21a2 100
857a7836
MT
101 # Perform the search
102 while True:
103 response = self.ldap.search_ext(search_base,
104 ldap.SCOPE_SUBTREE, query, attrlist=attrlist, sizelimit=limit,
105 serverctrls=[page_control],
106 )
18132fad 107
857a7836
MT
108 # Fetch all results
109 type, data, rmsgid, serverctrls = self.ldap.result3(response)
9137135a 110
857a7836
MT
111 # Append to local copy
112 results += data
113 pages += 1
9137135a 114
857a7836
MT
115 controls = [c for c in serverctrls
116 if c.controlType == ldap.controls.SimplePagedResultsControl.controlType]
9137135a 117
857a7836
MT
118 if not controls:
119 break
9137135a 120
857a7836
MT
121 # Set the cookie for more results
122 page_control.cookie = controls[0].cookie
9137135a 123
857a7836
MT
124 # There are no more results
125 if not page_control.cookie:
126 break
9137135a 127
857a7836
MT
128 # Log time it took to perform the query
129 log.debug("Query took %.2fms (%s page(s))" % ((time.time() - t) * 1000.0, pages))
f6e6ff79 130
857a7836
MT
131 # Return all attributes (without the DN)
132 return [attrs for dn, attrs in results]
f6e6ff79 133
857a7836
MT
134 def _ldap_get(self, *args, **kwargs):
135 results = self._ldap_query(*args, **kwargs)
f6e6ff79 136
857a7836
MT
137 # No result
138 if not results:
139 return {}
f6e6ff79 140
857a7836
MT
141 # Too many results?
142 elif len(results) > 1:
143 raise RuntimeException("Too many results returned for ldap_get()")
abac2d48 144
857a7836 145 return results[0]
abac2d48 146
857a7836
MT
147 def create(self, name, notify=False, _attrs=None):
148 """
149 Creates a new user
150 """
151 user = self._get_user("""
152 INSERT INTO
153 users(
154 name,
155 _attrs
156 )
157 VALUES
158 (%s, %s)
159 RETURNING
160 *
161 """, name, _attrs,
162 )
abac2d48 163
857a7836 164 log.debug("Created user %s" % user)
abac2d48 165
857a7836
MT
166 # Send a welcome email
167 if notify:
168 user._send_welcome_email()
040fc249 169
857a7836 170 return user
040fc249 171
857a7836
MT
172 def get_by_id(self, id):
173 return self._get_user("SELECT * FROM users \
174 WHERE id = %s", id)
040fc249 175
857a7836
MT
176 def get_by_name(self, name):
177 """
178 Fetch a user by its username
179 """
180 # Try to find a local user
181 user = self._get_user("""
182 SELECT
183 *
184 FROM
185 users
186 WHERE
187 deleted IS FALSE
188 AND
189 name = %s
190 """, name,
191 )
192 if user:
193 return user
194
195 # Search in LDAP
196 res = self._ldap_get(
197 "(&"
198 "(objectClass=person)"
199 "(uid=%s)"
200 ")" % name,
201 attrlist=("uid",),
202 )
203 if not res:
204 return
040fc249 205
857a7836
MT
206 # Fetch the UID
207 uid = res.get("uid")[0].decode()
040fc249 208
857a7836
MT
209 # Create a new user
210 return self.create(uid)
f6e6ff79 211
857a7836
MT
212 def get_by_email(self, email):
213 # Search in LDAP
214 res = self._ldap_get(
215 "(&"
216 "(objectClass=person)"
217 "(|"
218 "(mail=%s)"
219 "(mailAlternateAddress=%s)"
220 ")"
221 ")" % (email, email),
222 attrlist=("uid",),
223 )
f6e6ff79 224
857a7836
MT
225 # No results
226 if not res:
227 return
b9c2a52b 228
857a7836
MT
229 # Fetch the UID
230 uid = res.get("uid")[0].decode()
231
232 return self.get_by_name(uid)
233
234 def search(self, q, limit=None):
235 res = self._ldap_query(
236 "(&"
237 "(objectClass=person)"
238 "(|"
239 "(uid=%s)"
240 "(cn=*%s*)"
241 "(mail=%s)"
242 "(mailAlternateAddress=%s)"
243 ")"
244 ")" % (q, q, q, q),
245 attrlist=("uid",),
246 limit=limit,
247 )
b9c2a52b 248
857a7836
MT
249 # Fetch users
250 users = self._get_users("""
251 SELECT
252 *
253 FROM
254 users
255 WHERE
256 deleted IS FALSE
257 AND
258 name = ANY(%s)
259 """, [row.get("uid")[0].decode() for row in res],
260 )
f6e6ff79 261
857a7836 262 return sorted(users)
efbd7501 263
9137135a 264
b9c2a52b
MT
265class User(base.DataObject):
266 table = "users"
9137135a 267
20d7f5eb
MT
268 def __repr__(self):
269 return "<%s %s>" % (self.__class__.__name__, self.realname)
270
367bfa3a
MT
271 def __str__(self):
272 return self.realname
273
b0315eb4
MT
274 def __hash__(self):
275 return hash(self.id)
276
b9c2a52b
MT
277 def __lt__(self, other):
278 if isinstance(other, self.__class__):
279 return self.name < other.name
f6e6ff79 280
b9c2a52b
MT
281 elif isinstance(other, str):
282 return self.name < other
f6e6ff79 283
cd849b46
MT
284 return NotImplemented
285
857a7836
MT
286 @property
287 def name(self):
288 return self.data.name
289
9137135a 290 def delete(self):
b9c2a52b 291 self._set_attribute("deleted", True)
9137135a 292
5b261ed7
MT
293 # Destroy all sessions
294 for session in self.sessions:
295 session.destroy()
296
857a7836 297 # Fetch any attributes from LDAP
9137135a 298
857a7836
MT
299 @lazy_property
300 def attrs(self):
301 return self.backend.users._ldap_get("(uid=%s)" % self.name, attrlist=LDAP_ATTRS)
9137135a 302
857a7836
MT
303 def _get_attrs(self, key):
304 return [v.decode() for v in self.attrs.get(key, [])]
9137135a 305
857a7836
MT
306 def _get_attr(self, key):
307 for value in self._get_attrs(key):
308 return value
9137135a 309
857a7836 310 # Realname
f6e6ff79 311
d0bce25d 312 @property
857a7836
MT
313 def realname(self):
314 return self._get_attr("cn") or ""
b9c2a52b 315
26fe80df
JS
316 @property
317 def email(self):
857a7836
MT
318 """
319 The primary email address
320 """
321 return self._get_attr("email")
9137135a 322
23f86aae
MT
323 @property
324 def email_to(self):
325 """
326 The name/email address of the user in MIME format
327 """
857a7836 328 return email.utils.formataddr((self.name, self.email))
23f86aae 329
04a92018 330 def send_email(self, *args, **kwargs):
7b1479a1
MT
331 return self.backend.messages.send_template(
332 *args,
333 recipient=self,
334 locale=self.locale,
335 **kwargs,
336 )
68dd077d 337
18132fad
MT
338 def _send_welcome_email(self):
339 """
340 Sends a welcome email to the user
341 """
342 self.send_email("users/messages/welcome.txt")
343
b9c2a52b 344 def is_admin(self):
e304790f 345 return self.data.admin is True
9137135a 346
857a7836 347 # Locale
4947da2d 348
857a7836
MT
349 @property
350 def locale(self):
351 return tornado.locale.get()
9137135a 352
857a7836 353 # Timezone
9137135a 354
857a7836
MT
355 @property
356 def timezone(self, tz=None):
f6e6ff79
MT
357 if tz is None:
358 tz = self.data.timezone or ""
359
360 try:
361 tz = pytz.timezone(tz)
362 except pytz.UnknownTimeZoneError:
363 tz = pytz.timezone("UTC")
364
365 return tz
366
ba1958a5
JS
367 @property
368 def deleted(self):
369 return self.data.deleted
370
29256a69 371 # Avatar
9bf767c3 372
29256a69
MT
373 def avatar(self, size=512):
374 """
375 Returns a URL to the avatar the user has uploaded
376 """
377 return "https://people.ipfire.org/users/%s.jpg?size=%s" % (self.name, size)
9137135a 378
09d78b55
MT
379 # Permissions
380
381 def get_perms(self):
382 return self.data.perms
383
384 def set_perms(self, perms):
385 self._set_attribute("perms", perms or [])
386
387 perms = property(get_perms, set_perms)
f6e6ff79 388
bc373c9c 389 def has_perm(self, user):
f6e6ff79 390 """
bc373c9c
MT
391 Check, if the given user has the right to perform administrative
392 operations on this user.
f6e6ff79 393 """
bc373c9c
MT
394 # Anonymous people have no permission
395 if user is None:
396 return False
397
398 # Admins always have permission
399 if user.is_admin():
400 return True
401
402 # Users can edit themselves
403 if user == self:
f6e6ff79
MT
404 return True
405
bc373c9c
MT
406 # No permission
407 return False
f6e6ff79 408
b0315eb4
MT
409 @property
410 def sessions(self):
6c41a6f6
MT
411 sessions = self.backend.sessions._get_sessions("""
412 SELECT
413 *
414 FROM
415 sessions
416 WHERE
417 user_id = %s
418 AND
419 valid_until >= NOW()
420 ORDER BY
421 created_at
422 """, self.id,
423 )
424
425 return list(sessions)
b0315eb4 426
50533a78
MT
427 # Quota
428
429 def get_quota(self):
430 return self.data.quota
431
432 def set_quota(self, quota):
433 self._set_attribute("quota", quota)
434
435 quota = property(get_quota, set_quota)
436
437 def has_exceeded_quota(self, size=None):
438 """
439 Returns True if this user has exceeded their quota
440 """
441 # Skip quota check if this user has no quota
442 if not self.quota:
443 return
444
445 return self.disk_usage + (size or 0) >= self.quota
446
447 def check_quota(self, size=None):
448 """
449 Determines the user's disk usage
450 and raises an exception when the user is over quota.
451 """
452 # Raise QuotaExceededError if this user is over quota
453 if self.has_exceeded_quota(size=size):
454 raise QuotaExceededError
455
456 @lazy_property
457 def disk_usage(self):
458 """
459 Returns the total disk usage of this user
460 """
461 res = self.db.get("""
462 SELECT
463 disk_usage
464 FROM
465 user_disk_usages
466 WHERE
467 user_id = %s""",
468 self.id,
469 )
470
471 if res:
472 return res.disk_usage
473
474 return 0
475
f87d64a2
MT
476 # Custom repositories
477
478 @property
479 def repos(self):
480 """
481 Returns all custom repositories
482 """
5fc0715d 483 repos = self.backend.repos._get_repositories("""
f87d64a2
MT
484 SELECT
485 *
486 FROM
487 repositories
488 WHERE
489 deleted IS FALSE
490 AND
491 owner_id = %s
492 ORDER BY
493 name""",
494 self.id,
495 )
496
d1be120a
MT
497 distros = {}
498
499 # Group by distro
500 for repo in repos:
501 try:
502 distros[repo.distro].append(repo)
503 except KeyError:
504 distros[repo.distro] = [repo]
505
506 return distros
f87d64a2 507
940a8043 508 def get_repo(self, distro, slug):
258f9d20
MT
509 return self.backend.repos._get_repository("""
510 SELECT
511 *
512 FROM
513 repositories
514 WHERE
515 deleted IS FALSE
516 AND
517 owner_id = %s
940a8043
MT
518 AND
519 distro_id = %s
258f9d20
MT
520 AND
521 slug = %s""",
522 self.id,
940a8043 523 distro,
258f9d20
MT
524 slug,
525 )
526
b9c2a52b 527
50533a78
MT
528class QuotaExceededError(Exception):
529 pass