]> git.ipfire.org Git - ipfire.org.git/blob - src/web/people.py
people: Drop SSH keys
[ipfire.org.git] / src / web / people.py
1 #!/usr/bin/python
2
3 import datetime
4 import ldap
5 import logging
6 import imghdr
7 import tornado.web
8 import urllib.parse
9
10 from .. import countries
11
12 from . import auth
13 from . import base
14 from . import ui_modules
15
16 class IndexHandler(auth.CacheMixin, base.BaseHandler):
17 @tornado.web.authenticated
18 def get(self):
19 self.render("people/index.html")
20
21
22 class AvatarHandler(base.BaseHandler):
23 def get(self, uid):
24 # Get the desired size of the avatar file
25 size = self.get_argument("size", None)
26
27 try:
28 size = int(size)
29 except (TypeError, ValueError):
30 size = None
31
32 logging.debug("Querying for avatar of %s" % uid)
33
34 # Fetch user account
35 account = self.backend.accounts.get_by_uid(uid)
36 if not account:
37 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
38
39 # Allow downstream to cache this for 60 minutes
40 self.set_expires(3600)
41
42 # Resize avatar
43 avatar = account.get_avatar(size)
44
45 # If there is no avatar, we serve a default image
46 if not avatar:
47 logging.debug("No avatar uploaded for %s" % account)
48
49 return self.redirect(self.static_url("img/default-avatar.jpg"))
50
51 # Guess content type
52 type = imghdr.what(None, avatar)
53
54 # Set headers about content
55 self.set_header("Content-Disposition", "inline; filename=\"%s.%s\"" % (account.uid, type))
56 self.set_header("Content-Type", "image/%s" % type)
57
58 # Deliver payload
59 self.finish(avatar)
60
61
62 class CallsHandler(auth.CacheMixin, base.BaseHandler):
63 @tornado.web.authenticated
64 def get(self, uid, date=None):
65 account = self.backend.accounts.get_by_uid(uid)
66 if not account:
67 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
68
69 # Check for permissions
70 if not account.can_be_managed_by(self.current_user):
71 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
72
73 if date:
74 try:
75 date = datetime.datetime.strptime(date, "%Y-%m-%d").date()
76 except ValueError:
77 raise tornado.web.HTTPError(400, "Invalid date: %s" % date)
78 else:
79 date = datetime.date.today()
80
81 self.render("people/calls.html", account=account, date=date)
82
83
84 class CallHandler(auth.CacheMixin, base.BaseHandler):
85 @tornado.web.authenticated
86 def get(self, uid, uuid):
87 account = self.backend.accounts.get_by_uid(uid)
88 if not account:
89 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
90
91 # Check for permissions
92 if not account.can_be_managed_by(self.current_user):
93 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
94
95 call = self.backend.talk.freeswitch.get_call_by_uuid(uuid)
96 if not call:
97 raise tornado.web.HTTPError(404, "Could not find call %s" % uuid)
98
99 # XXX limit
100
101 self.render("people/call.html", account=account, call=call)
102
103
104 class ConferencesHandler(auth.CacheMixin, base.BaseHandler):
105 @tornado.web.authenticated
106 def get(self):
107 self.render("people/conferences.html", conferences=self.backend.talk.conferences)
108
109
110 class SearchHandler(auth.CacheMixin, base.BaseHandler):
111 @tornado.web.authenticated
112 def get(self):
113 q = self.get_argument("q")
114
115 # Perform the search
116 accounts = self.backend.accounts.search(q)
117
118 # Redirect when only one result was found
119 if len(accounts) == 1:
120 self.redirect("/users/%s" % accounts[0].uid)
121 return
122
123 self.render("people/search.html", q=q, accounts=accounts)
124
125
126 class SIPHandler(auth.CacheMixin, base.BaseHandler):
127 @tornado.web.authenticated
128 def get(self, uid):
129 account = self.backend.accounts.get_by_uid(uid)
130 if not account:
131 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
132
133 # Check for permissions
134 if not account.can_be_managed_by(self.current_user):
135 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
136
137 self.render("people/sip.html", account=account)
138
139
140 class UsersHandler(auth.CacheMixin, base.BaseHandler):
141 @tornado.web.authenticated
142 def get(self):
143 # Only staff can see other users
144 if not self.current_user.is_staff():
145 raise tornado.web.HTTPError(403)
146
147 self.render("people/users.html")
148
149
150 class UserHandler(auth.CacheMixin, base.BaseHandler):
151 @tornado.web.authenticated
152 def get(self, uid):
153 account = self.backend.accounts.get_by_uid(uid)
154 if not account:
155 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
156
157 self.render("people/user.html", account=account)
158
159
160 class UserEditHandler(auth.CacheMixin, base.BaseHandler):
161 @tornado.web.authenticated
162 def get(self, uid):
163 account = self.backend.accounts.get_by_uid(uid)
164 if not account:
165 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
166
167 # Check for permissions
168 if not account.can_be_managed_by(self.current_user):
169 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
170
171 self.render("people/user-edit.html", account=account, countries=countries.get_all())
172
173 @tornado.web.authenticated
174 def post(self, uid):
175 account = self.backend.accounts.get_by_uid(uid)
176 if not account:
177 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
178
179 # Check for permissions
180 if not account.can_be_managed_by(self.current_user):
181 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
182
183 # Unfortunately this cannot be wrapped into a transaction
184 try:
185 account.first_name = self.get_argument("first_name")
186 account.last_name = self.get_argument("last_name")
187 account.nickname = self.get_argument("nickname", None)
188 account.street = self.get_argument("street", None)
189 account.city = self.get_argument("city", None)
190 account.postal_code = self.get_argument("postal_code", None)
191 account.country_code = self.get_argument("country_code", None)
192
193 # Avatar
194 try:
195 filename, data, mimetype = self.get_file("avatar")
196
197 if not mimetype.startswith("image/"):
198 raise tornado.web.HTTPError(400, "Avatar is not an image file: %s" % mimetype)
199
200 account.upload_avatar(data)
201 except TypeError:
202 pass
203
204 # Email
205 account.mail_routing_address = self.get_argument("mail_routing_address", None)
206
207 # Telephone
208 account.phone_numbers = self.get_argument("phone_numbers", "").splitlines()
209 account.sip_routing_address = self.get_argument("sip_routing_address", None)
210 except ldap.STRONG_AUTH_REQUIRED as e:
211 raise tornado.web.HTTPError(403, "%s" % e) from e
212
213 # Redirect back to user page
214 self.redirect("/users/%s" % account.uid)
215
216
217 class UserPasswdHandler(auth.CacheMixin, base.BaseHandler):
218 @tornado.web.authenticated
219 def get(self, uid):
220 account = self.backend.accounts.get_by_uid(uid)
221 if not account:
222 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
223
224 # Check for permissions
225 if not account.can_be_managed_by(self.current_user):
226 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
227
228 self.render("people/passwd.html", account=account)
229
230 @tornado.web.authenticated
231 def post(self, uid):
232 account = self.backend.accounts.get_by_uid(uid)
233 if not account:
234 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
235
236 # Check for permissions
237 if not account.can_be_managed_by(self.current_user):
238 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
239
240 # Get current password
241 password = self.get_argument("password")
242
243 # Get new password
244 password1 = self.get_argument("password1")
245 password2 = self.get_argument("password2")
246
247 # Passwords must match
248 if not password1 == password2:
249 raise tornado.web.HTTPError(400, "Passwords do not match")
250
251 # XXX Check password complexity
252
253 # Check if old password matches
254 if not account.check_password(password):
255 raise tornado.web.HTTPError(403, "Incorrect password for %s" % account)
256
257 # Save new password
258 account.passwd(password1)
259
260 # Redirect back to user's page
261 self.redirect("/users/%s" % account.uid)
262
263
264 class SSODiscourse(auth.CacheMixin, base.BaseHandler):
265 def _get_discourse_params(self):
266 # Fetch Discourse's parameters
267 sso = self.get_argument("sso")
268 sig = self.get_argument("sig")
269
270 # Decode payload
271 try:
272 return self.accounts.decode_discourse_payload(sso, sig)
273
274 # Raise bad request if the signature is invalid
275 except ValueError:
276 raise tornado.web.HTTPError(400)
277
278 def _redirect_user_to_discourse(self, account, nonce, return_sso_url):
279 """
280 Redirects the user back to Discourse passing some
281 attributes of the user account to Discourse
282 """
283 args = {
284 "nonce" : nonce,
285 "external_id" : account.uid,
286
287 # Pass email address
288 "email" : account.email,
289 "require_activation" : "false",
290
291 # More details about the user
292 "username" : account.uid,
293 "name" : "%s" % account,
294
295 # Avatar
296 "avatar_url" : account.avatar_url(),
297 "avatar_force_update" : "true",
298
299 # Send a welcome message
300 "suppress_welcome_message" : "false",
301
302 # Group memberships
303 "admin" : "true" if account.is_admin() else "false",
304 "moderator" : "true" if account.is_staff() else "false",
305 }
306
307 # Format payload and sign it
308 payload = self.accounts.encode_discourse_payload(**args)
309 signature = self.accounts.sign_discourse_payload(payload)
310
311 qs = urllib.parse.urlencode({
312 "sso" : payload,
313 "sig" : signature,
314 })
315
316 # Redirect user
317 self.redirect("%s?%s" % (return_sso_url, qs))
318
319 @base.ratelimit(minutes=24*60, requests=100)
320 def get(self):
321 params = self._get_discourse_params()
322
323 # Redirect back if user is already logged in
324 if self.current_user:
325 return self._redirect_user_to_discourse(self.current_user, **params)
326
327 # Otherwise the user needs to authenticate
328 self.render("auth/login.html", next=None)
329
330 @base.ratelimit(minutes=24*60, requests=100)
331 def post(self):
332 params = self._get_discourse_params()
333
334 # Get credentials
335 username = self.get_argument("username")
336 password = self.get_argument("password")
337
338 # Check credentials
339 account = self.accounts.auth(username, password)
340 if not account:
341 raise tornado.web.HTTPError(401, "Unknown user or invalid password: %s" % username)
342
343 # If the user has been authenticated, we will redirect to Discourse
344 self._redirect_user_to_discourse(account, **params)
345
346
347 class NewAccountsModule(ui_modules.UIModule):
348 def render(self, days=14):
349 t = datetime.datetime.utcnow() - datetime.timedelta(days=days)
350
351 # Fetch all accounts created after t
352 accounts = self.backend.accounts.get_created_after(t)
353
354 accounts.sort(key=lambda a: a.created_at, reverse=True)
355
356 return self.render_string("people/modules/accounts-new.html",
357 accounts=accounts, t=t)
358
359
360 class AccountsListModule(ui_modules.UIModule):
361 def render(self, accounts=None):
362 if accounts is None:
363 accounts = self.backend.accounts
364
365 return self.render_string("people/modules/accounts-list.html", accounts=accounts)
366
367
368 class CDRModule(ui_modules.UIModule):
369 def render(self, account, date=None, limit=None):
370 cdr = account.get_cdr(date=date, limit=limit)
371
372 return self.render_string("people/modules/cdr.html",
373 account=account, cdr=list(cdr))
374
375
376 class ChannelsModule(ui_modules.UIModule):
377 def render(self, account):
378 return self.render_string("people/modules/channels.html",
379 account=account, channels=account.sip_channels)
380
381
382 class MOSModule(ui_modules.UIModule):
383 def render(self, call):
384 return self.render_string("people/modules/mos.html", call=call)
385
386
387 class PasswordModule(ui_modules.UIModule):
388 def render(self, account=None):
389 return self.render_string("people/modules/password.html", account=account)
390
391 def javascript_files(self):
392 return "js/zxcvbn.js"
393
394 def embedded_javascript(self):
395 return self.render_string("people/modules/password.js")
396
397
398 class RegistrationsModule(ui_modules.UIModule):
399 def render(self, account):
400 return self.render_string("people/modules/registrations.html", account=account)
401
402
403 class SIPStatusModule(ui_modules.UIModule):
404 def render(self, account):
405 return self.render_string("people/modules/sip-status.html", account=account)