]>
Commit | Line | Data |
---|---|---|
2cd9af74 MT |
1 | #!/usr/bin/python |
2 | ||
bdaf6b46 | 3 | import datetime |
e96e445b | 4 | import ldap |
2cd9af74 | 5 | import logging |
020de883 | 6 | import imghdr |
0d1fb712 | 7 | import sshpubkeys |
2cd9af74 | 8 | import tornado.web |
2dac7110 | 9 | import urllib.parse |
2cd9af74 | 10 | |
0099c2a7 MT |
11 | from .. import countries |
12 | ||
9b8ff27d | 13 | from . import auth |
124a8404 | 14 | from . import base |
786e9ca8 MT |
15 | from . import ui_modules |
16 | ||
9b8ff27d | 17 | class IndexHandler(auth.CacheMixin, base.BaseHandler): |
786e9ca8 MT |
18 | @tornado.web.authenticated |
19 | def get(self): | |
20 | self.render("people/index.html") | |
21 | ||
2cd9af74 | 22 | |
c4f1a618 | 23 | class AvatarHandler(base.BaseHandler): |
0cddf220 | 24 | def get(self, uid): |
2cd9af74 MT |
25 | # Get the desired size of the avatar file |
26 | size = self.get_argument("size", 0) | |
27 | ||
28 | try: | |
29 | size = int(size) | |
30 | except (TypeError, ValueError): | |
31 | size = None | |
32 | ||
0cddf220 | 33 | logging.debug("Querying for avatar of %s" % uid) |
2cd9af74 | 34 | |
f6ed3d4d | 35 | # Fetch user account |
0cddf220 | 36 | account = self.backend.accounts.get_by_uid(uid) |
f6ed3d4d | 37 | if not account: |
0cddf220 | 38 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) |
2cd9af74 | 39 | |
f6ed3d4d MT |
40 | # Allow downstream to cache this for 60 minutes |
41 | self.set_expires(3600) | |
2cd9af74 | 42 | |
f6ed3d4d MT |
43 | # Resize avatar |
44 | avatar = account.get_avatar(size) | |
2cd9af74 | 45 | |
f6ed3d4d MT |
46 | # If there is no avatar, we serve a default image |
47 | if not avatar: | |
0cddf220 MT |
48 | logging.debug("No avatar uploaded for %s" % account) |
49 | ||
3f9f12f0 | 50 | return self.redirect(self.static_url("img/default-avatar.jpg")) |
2cd9af74 | 51 | |
020de883 MT |
52 | # Guess content type |
53 | type = imghdr.what(None, avatar) | |
54 | ||
f6ed3d4d | 55 | # Set headers about content |
020de883 MT |
56 | self.set_header("Content-Disposition", "inline; filename=\"%s.%s\"" % (account.uid, type)) |
57 | self.set_header("Content-Type", "image/%s" % type) | |
2cd9af74 | 58 | |
f6ed3d4d | 59 | # Deliver payload |
2cd9af74 | 60 | self.finish(avatar) |
786e9ca8 MT |
61 | |
62 | ||
9b8ff27d | 63 | class CallsHandler(auth.CacheMixin, base.BaseHandler): |
bdaf6b46 MT |
64 | @tornado.web.authenticated |
65 | def get(self, uid, date=None): | |
66 | account = self.backend.accounts.get_by_uid(uid) | |
67 | if not account: | |
68 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
69 | ||
d561d931 MT |
70 | # Check for permissions |
71 | if not account.can_be_managed_by(self.current_user): | |
72 | raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account)) | |
73 | ||
bdaf6b46 | 74 | if date: |
44b4640b MT |
75 | try: |
76 | date = datetime.datetime.strptime(date, "%Y-%m-%d").date() | |
77 | except ValueError: | |
78 | raise tornado.web.HTTPError(400, "Invalid date: %s" % date) | |
bdaf6b46 MT |
79 | else: |
80 | date = datetime.date.today() | |
81 | ||
82 | self.render("people/calls.html", account=account, date=date) | |
83 | ||
84 | ||
9b8ff27d | 85 | class CallHandler(auth.CacheMixin, base.BaseHandler): |
68ece434 MT |
86 | @tornado.web.authenticated |
87 | def get(self, uid, uuid): | |
d09d554b MT |
88 | account = self.backend.accounts.get_by_uid(uid) |
89 | if not account: | |
90 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
91 | ||
d561d931 MT |
92 | # Check for permissions |
93 | if not account.can_be_managed_by(self.current_user): | |
94 | raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account)) | |
95 | ||
68ece434 MT |
96 | call = self.backend.talk.freeswitch.get_call_by_uuid(uuid) |
97 | if not call: | |
98 | raise tornado.web.HTTPError(404, "Could not find call %s" % uuid) | |
99 | ||
100 | # XXX limit | |
101 | ||
d09d554b | 102 | self.render("people/call.html", account=account, call=call) |
68ece434 MT |
103 | |
104 | ||
9b8ff27d | 105 | class ConferencesHandler(auth.CacheMixin, base.BaseHandler): |
30aeccdb MT |
106 | @tornado.web.authenticated |
107 | def get(self): | |
108 | self.render("people/conferences.html", conferences=self.backend.talk.conferences) | |
109 | ||
110 | ||
9b8ff27d | 111 | class SearchHandler(auth.CacheMixin, base.BaseHandler): |
786e9ca8 MT |
112 | @tornado.web.authenticated |
113 | def get(self): | |
114 | q = self.get_argument("q") | |
115 | ||
116 | # Perform the search | |
51907e45 | 117 | accounts = self.backend.accounts.search(q) |
786e9ca8 MT |
118 | |
119 | # Redirect when only one result was found | |
120 | if len(accounts) == 1: | |
121 | self.redirect("/users/%s" % accounts[0].uid) | |
122 | return | |
123 | ||
124 | self.render("people/search.html", q=q, accounts=accounts) | |
125 | ||
f4672785 | 126 | |
9b8ff27d | 127 | class SSHKeysIndexHandler(auth.CacheMixin, base.BaseHandler): |
f4672785 MT |
128 | @tornado.web.authenticated |
129 | def get(self, uid): | |
130 | account = self.backend.accounts.get_by_uid(uid) | |
131 | if not account: | |
132 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
133 | ||
134 | self.render("people/ssh-keys/index.html", account=account) | |
135 | ||
55b67ca4 | 136 | |
9b8ff27d | 137 | class SSHKeysDownloadHandler(auth.CacheMixin, base.BaseHandler): |
55b67ca4 | 138 | @tornado.web.authenticated |
44b75370 | 139 | def get(self, uid, hash_sha256): |
55b67ca4 MT |
140 | account = self.backend.accounts.get_by_uid(uid) |
141 | if not account: | |
142 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
143 | ||
144 | # Get SSH key | |
44b75370 | 145 | key = account.get_ssh_key_by_hash_sha256(hash_sha256) |
55b67ca4 | 146 | if not key: |
44b75370 | 147 | raise tornado.web.HTTPError(404, "Could not find key: %s" % hash_sha256) |
55b67ca4 MT |
148 | |
149 | # Set HTTP Headers | |
150 | self.add_header("Content-Type", "text/plain") | |
151 | ||
152 | self.finish(key.keydata) | |
153 | ||
0d1fb712 | 154 | |
9b8ff27d | 155 | class SSHKeysUploadHandler(auth.CacheMixin, base.BaseHandler): |
0d1fb712 MT |
156 | @tornado.web.authenticated |
157 | def get(self, uid): | |
158 | account = self.backend.accounts.get_by_uid(uid) | |
159 | if not account: | |
160 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
161 | ||
162 | # Check for permissions | |
163 | if not account.can_be_managed_by(self.current_user): | |
164 | raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account)) | |
165 | ||
166 | self.render("people/ssh-keys/upload.html", account=account) | |
167 | ||
168 | @tornado.web.authenticated | |
169 | def post(self, uid): | |
170 | account = self.backend.accounts.get_by_uid(uid) | |
171 | if not account: | |
172 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
173 | ||
174 | # Check for permissions | |
175 | if not account.can_be_managed_by(self.current_user): | |
176 | raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account)) | |
177 | ||
178 | key = self.get_argument("key") | |
179 | ||
180 | # Verify password | |
181 | password = self.get_argument("password") | |
182 | if not account.check_password(password): | |
183 | raise tornado.web.HTTPError(403, "Incorrect password for %s" % account) | |
184 | ||
185 | # Try to add new SSH key | |
186 | try: | |
187 | account.add_ssh_key(key) | |
188 | ||
189 | except sshpubkeys.InvalidKeyException as e: | |
190 | self.render("people/ssh-keys/error-invalid-key.html", account=account, exception=e) | |
191 | return | |
192 | ||
193 | self.redirect("/users/%s/ssh-keys" % account.uid) | |
194 | ||
195 | ||
9b8ff27d | 196 | class SSHKeysDeleteHandler(auth.CacheMixin, base.BaseHandler): |
0d1fb712 | 197 | @tornado.web.authenticated |
44b75370 | 198 | def get(self, uid, hash_sha256): |
0d1fb712 MT |
199 | account = self.backend.accounts.get_by_uid(uid) |
200 | if not account: | |
201 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
202 | ||
203 | # Get SSH key | |
44b75370 | 204 | key = account.get_ssh_key_by_hash_sha256(hash_sha256) |
0d1fb712 | 205 | if not key: |
44b75370 | 206 | raise tornado.web.HTTPError(404, "Could not find key: %s" % hash_sha256) |
0d1fb712 MT |
207 | |
208 | self.render("people/ssh-keys/delete.html", account=account, key=key) | |
209 | ||
210 | @tornado.web.authenticated | |
44b75370 | 211 | def post(self, uid, hash_sha256): |
0d1fb712 MT |
212 | account = self.backend.accounts.get_by_uid(uid) |
213 | if not account: | |
214 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
215 | ||
216 | # Get SSH key | |
44b75370 | 217 | key = account.get_ssh_key_by_hash_sha256(hash_sha256) |
0d1fb712 | 218 | if not key: |
44b75370 | 219 | raise tornado.web.HTTPError(404, "Could not find key: %s" % hash_sha256) |
0d1fb712 MT |
220 | |
221 | # Verify password | |
222 | password = self.get_argument("password") | |
223 | if not account.check_password(password): | |
224 | raise tornado.web.HTTPError(403, "Incorrect password for %s" % account) | |
225 | ||
226 | # Delete the key | |
227 | account.delete_ssh_key(key.keydata) | |
228 | ||
229 | self.redirect("/users/%s/ssh-keys" % account.uid) | |
230 | ||
786e9ca8 | 231 | |
9b8ff27d | 232 | class SIPHandler(auth.CacheMixin, base.BaseHandler): |
e0daee8f MT |
233 | @tornado.web.authenticated |
234 | def get(self, uid): | |
235 | account = self.backend.accounts.get_by_uid(uid) | |
236 | if not account: | |
237 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
238 | ||
239 | # Check for permissions | |
240 | if not account.can_be_managed_by(self.current_user): | |
241 | raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account)) | |
242 | ||
243 | self.render("people/sip.html", account=account) | |
244 | ||
245 | ||
9b8ff27d | 246 | class UsersHandler(auth.CacheMixin, base.BaseHandler): |
786e9ca8 MT |
247 | @tornado.web.authenticated |
248 | def get(self): | |
71a3109c MT |
249 | # Only staff can see other users |
250 | if not self.current_user.is_staff(): | |
251 | raise tornado.web.HTTPError(403) | |
252 | ||
786e9ca8 MT |
253 | self.render("people/users.html") |
254 | ||
255 | ||
9b8ff27d | 256 | class UserHandler(auth.CacheMixin, base.BaseHandler): |
786e9ca8 MT |
257 | @tornado.web.authenticated |
258 | def get(self, uid): | |
259 | account = self.backend.accounts.get_by_uid(uid) | |
260 | if not account: | |
261 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
262 | ||
263 | self.render("people/user.html", account=account) | |
264 | ||
265 | ||
9b8ff27d | 266 | class UserEditHandler(auth.CacheMixin, base.BaseHandler): |
e96e445b MT |
267 | @tornado.web.authenticated |
268 | def get(self, uid): | |
269 | account = self.backend.accounts.get_by_uid(uid) | |
270 | if not account: | |
271 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
272 | ||
273 | # Check for permissions | |
274 | if not account.can_be_managed_by(self.current_user): | |
275 | raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account)) | |
276 | ||
0099c2a7 | 277 | self.render("people/user-edit.html", account=account, countries=countries.get_all()) |
e96e445b MT |
278 | |
279 | @tornado.web.authenticated | |
280 | def post(self, uid): | |
281 | account = self.backend.accounts.get_by_uid(uid) | |
282 | if not account: | |
283 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
284 | ||
285 | # Check for permissions | |
286 | if not account.can_be_managed_by(self.current_user): | |
287 | raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account)) | |
288 | ||
289 | # Unfortunately this cannot be wrapped into a transaction | |
290 | try: | |
0099c2a7 MT |
291 | account.first_name = self.get_argument("first_name") |
292 | account.last_name = self.get_argument("last_name") | |
d6e57f73 | 293 | account.nickname = self.get_argument("nickname", None) |
0099c2a7 MT |
294 | account.street = self.get_argument("street", None) |
295 | account.city = self.get_argument("city", None) | |
296 | account.postal_code = self.get_argument("postal_code", None) | |
297 | account.country_code = self.get_argument("country_code", None) | |
e96e445b | 298 | |
5cc10421 MT |
299 | # Avatar |
300 | try: | |
301 | filename, data, mimetype = self.get_file("avatar") | |
302 | ||
303 | if not mimetype.startswith("image/"): | |
304 | raise tornado.web.HTTPError(400, "Avatar is not an image file: %s" % mimetype) | |
305 | ||
306 | account.upload_avatar(data) | |
9bbf48b8 | 307 | except TypeError: |
5cc10421 MT |
308 | pass |
309 | ||
e96e445b MT |
310 | |
311 | account.mail_routing_address = self.get_argument("mail_routing_address", None) | |
312 | ||
313 | # Telephone | |
314 | account.phone_numbers = self.get_argument("phone_numbers", "").splitlines() | |
315 | account.sip_routing_address = self.get_argument("sip_routing_address", None) | |
316 | except ldap.STRONG_AUTH_REQUIRED as e: | |
317 | raise tornado.web.HTTPError(403, "%s" % e) from e | |
318 | ||
319 | # Redirect back to user page | |
320 | self.redirect("/users/%s" % account.uid) | |
321 | ||
322 | ||
9b8ff27d | 323 | class UserPasswdHandler(auth.CacheMixin, base.BaseHandler): |
3ea97943 MT |
324 | @tornado.web.authenticated |
325 | def get(self, uid): | |
326 | account = self.backend.accounts.get_by_uid(uid) | |
327 | if not account: | |
328 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
329 | ||
330 | # Check for permissions | |
331 | if not account.can_be_managed_by(self.current_user): | |
332 | raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account)) | |
333 | ||
334 | self.render("people/passwd.html", account=account) | |
335 | ||
336 | @tornado.web.authenticated | |
337 | def post(self, uid): | |
338 | account = self.backend.accounts.get_by_uid(uid) | |
339 | if not account: | |
340 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
341 | ||
342 | # Check for permissions | |
343 | if not account.can_be_managed_by(self.current_user): | |
344 | raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account)) | |
345 | ||
346 | # Get current password | |
347 | password = self.get_argument("password") | |
348 | ||
349 | # Get new password | |
350 | password1 = self.get_argument("password1") | |
351 | password2 = self.get_argument("password2") | |
352 | ||
353 | # Passwords must match | |
354 | if not password1 == password2: | |
355 | raise tornado.web.HTTPError(400, "Passwords do not match") | |
356 | ||
357 | # XXX Check password complexity | |
358 | ||
359 | # Check if old password matches | |
360 | if not account.check_password(password): | |
361 | raise tornado.web.HTTPError(403, "Incorrect password for %s" % account) | |
362 | ||
363 | # Save new password | |
364 | account.passwd(password1) | |
365 | ||
366 | # Redirect back to user's page | |
367 | self.redirect("/users/%s" % account.uid) | |
368 | ||
369 | ||
2dac7110 MT |
370 | class SSODiscourse(auth.CacheMixin, base.BaseHandler): |
371 | def _get_discourse_params(self): | |
372 | # Fetch Discourse's parameters | |
373 | sso = self.get_argument("sso") | |
374 | sig = self.get_argument("sig") | |
375 | ||
376 | # Decode payload | |
377 | try: | |
378 | return self.accounts.decode_discourse_payload(sso, sig) | |
379 | ||
380 | # Raise bad request if the signature is invalid | |
381 | except ValueError: | |
382 | raise tornado.web.HTTPError(400) | |
383 | ||
384 | def _redirect_user_to_discourse(self, account, nonce, return_sso_url): | |
385 | """ | |
386 | Redirects the user back to Discourse passing some | |
387 | attributes of the user account to Discourse | |
388 | """ | |
389 | args = { | |
390 | "nonce" : nonce, | |
391 | "external_id" : account.uid, | |
392 | ||
393 | # Pass email address | |
394 | "email" : account.email, | |
395 | "require_activation" : "false", | |
396 | ||
397 | # More details about the user | |
398 | "username" : account.uid, | |
399 | "name" : "%s" % account, | |
400 | ||
401 | # Avatar | |
402 | "avatar_url" : account.avatar_url(), | |
403 | "avatar_force_update" : "true", | |
404 | ||
405 | # Send a welcome message | |
406 | "suppress_welcome_message" : "false", | |
407 | ||
408 | # Group memberships | |
409 | "admin" : "true" if account.is_admin() else "false", | |
410 | "moderator" : "true" if account.is_staff() else "false", | |
411 | } | |
412 | ||
413 | # Format payload and sign it | |
414 | payload = self.accounts.encode_discourse_payload(**args) | |
415 | signature = self.accounts.sign_discourse_payload(payload) | |
416 | ||
417 | qs = urllib.parse.urlencode({ | |
418 | "sso" : payload, | |
419 | "sig" : signature, | |
420 | }) | |
421 | ||
422 | # Redirect user | |
423 | self.redirect("%s?%s" % (return_sso_url, qs)) | |
424 | ||
425 | @base.ratelimit(minutes=24*60, requests=100) | |
426 | def get(self): | |
427 | params = self._get_discourse_params() | |
428 | ||
429 | # Redirect back if user is already logged in | |
a2de2e1f MT |
430 | if self.current_user: |
431 | return self._redirect_user_to_discourse(self.current_user, **params) | |
2dac7110 MT |
432 | |
433 | # Otherwise the user needs to authenticate | |
328a7710 MT |
434 | self.render("auth/login.html", next=None) |
435 | ||
436 | @base.ratelimit(minutes=24*60, requests=100) | |
437 | def post(self): | |
438 | params = self._get_discourse_params() | |
439 | ||
440 | # Get credentials | |
441 | username = self.get_argument("username") | |
442 | password = self.get_argument("password") | |
443 | ||
444 | # Check credentials | |
445 | account = self.accounts.auth(username, password) | |
446 | if not account: | |
447 | raise tornado.web.HTTPError(401, "Unknown user or invalid password: %s" % username) | |
448 | ||
449 | # If the user has been authenticated, we will redirect to Discourse | |
450 | self._redirect_user_to_discourse(account, **params) | |
2dac7110 MT |
451 | |
452 | ||
9150881e MT |
453 | class NewAccountsModule(ui_modules.UIModule): |
454 | def render(self, days=14): | |
455 | t = datetime.datetime.utcnow() - datetime.timedelta(days=days) | |
456 | ||
457 | # Fetch all accounts created after t | |
458 | accounts = self.backend.accounts.get_created_after(t) | |
459 | ||
460 | accounts.sort(key=lambda a: a.created_at, reverse=True) | |
461 | ||
462 | return self.render_string("people/modules/accounts-new.html", | |
463 | accounts=accounts, t=t) | |
464 | ||
465 | ||
786e9ca8 MT |
466 | class AccountsListModule(ui_modules.UIModule): |
467 | def render(self, accounts=None): | |
468 | if accounts is None: | |
51907e45 | 469 | accounts = self.backend.accounts |
786e9ca8 MT |
470 | |
471 | return self.render_string("people/modules/accounts-list.html", accounts=accounts) | |
472 | ||
473 | ||
474 | class CDRModule(ui_modules.UIModule): | |
bdaf6b46 MT |
475 | def render(self, account, date=None, limit=None): |
476 | cdr = account.get_cdr(date=date, limit=limit) | |
786e9ca8 | 477 | |
89e47299 MT |
478 | return self.render_string("people/modules/cdr.html", |
479 | account=account, cdr=list(cdr)) | |
786e9ca8 MT |
480 | |
481 | ||
482 | class ChannelsModule(ui_modules.UIModule): | |
483 | def render(self, account): | |
1f38be5a MT |
484 | return self.render_string("people/modules/channels.html", |
485 | account=account, channels=account.sip_channels) | |
786e9ca8 MT |
486 | |
487 | ||
68ece434 MT |
488 | class MOSModule(ui_modules.UIModule): |
489 | def render(self, call): | |
490 | return self.render_string("people/modules/mos.html", call=call) | |
491 | ||
492 | ||
b5e2077f | 493 | class PasswordModule(ui_modules.UIModule): |
56894a8b | 494 | def render(self, account=None): |
b5e2077f MT |
495 | return self.render_string("people/modules/password.html", account=account) |
496 | ||
497 | def javascript_files(self): | |
498 | return "js/zxcvbn.js" | |
499 | ||
500 | def embedded_javascript(self): | |
501 | return self.render_string("people/modules/password.js") | |
502 | ||
503 | ||
786e9ca8 MT |
504 | class RegistrationsModule(ui_modules.UIModule): |
505 | def render(self, account): | |
506 | return self.render_string("people/modules/registrations.html", account=account) | |
7afd64bb MT |
507 | |
508 | ||
509 | class SIPStatusModule(ui_modules.UIModule): | |
510 | def render(self, account): | |
511 | return self.render_string("people/modules/sip-status.html", account=account) |