]>
Commit | Line | Data |
---|---|---|
857a7836 | 1 | #!/usr/bin/python3 |
9137135a | 2 | |
98b826d4 | 3 | import datetime |
b9c2a52b | 4 | import email.utils |
857a7836 | 5 | import ldap |
9137135a | 6 | import logging |
f6e6ff79 | 7 | import pytz |
857a7836 | 8 | import time |
9137135a MT |
9 | |
10 | import tornado.locale | |
11 | ||
2c909128 | 12 | from . import base |
9137135a | 13 | |
b9c2a52b MT |
14 | from .decorators import * |
15 | ||
a329017a MT |
16 | # Setup logging |
17 | log = logging.getLogger("pakfire.builservice.users") | |
18 | ||
857a7836 MT |
19 | # A list of LDAP attributes that we fetch |
20 | LDAP_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 | 35 | class 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 |
265 | class 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 |
528 | class QuotaExceededError(Exception): |
529 | pass |