1 # This file is part of Indico.
2 # Copyright (C) 2002 - 2015 European Organization for Nuclear Research (CERN).
4 # Indico is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License as
6 # published by the Free Software Foundation; either version 3 of the
7 # License, or (at your option) any later version.
9 # Indico is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with Indico; if not, see <http://www.gnu.org/licenses/>.
17 from __future__
import unicode_literals
19 from flask
import session
, redirect
, request
, flash
, render_template
, jsonify
20 from itsdangerous
import BadData
21 from markupsafe
import Markup
22 from werkzeug
.exceptions
import BadRequest
, Forbidden
, NotFound
24 from indico
.core
import signals
25 from indico
.core
.auth
import multipass
26 from indico
.core
.config
import Config
27 from indico
.core
.db
import db
28 from indico
.core
.notifications
import make_email
29 from indico
.modules
.auth
import logger
, Identity
, login_user
30 from indico
.modules
.auth
.forms
import (SelectEmailForm
, MultipassRegistrationForm
, LocalRegistrationForm
,
31 RegistrationEmailForm
, ResetPasswordEmailForm
, ResetPasswordForm
,
32 AddLocalIdentityForm
, EditLocalIdentityForm
)
33 from indico
.modules
.auth
.util
import load_identity_info
34 from indico
.modules
.auth
.views
import WPAuth
35 from indico
.modules
.users
import User
36 from indico
.modules
.users
.controllers
import RHUserBase
37 from indico
.modules
.users
.views
import WPUser
38 from indico
.util
.i18n
import _
39 from indico
.util
.signing
import secure_serializer
40 from indico
.web
.flask
.util
import url_for
41 from indico
.web
.flask
.templating
import get_template_module
42 from indico
.web
.forms
.base
import FormDefaults
, IndicoForm
44 from MaKaC
.common
import HelperMaKaCInfo
45 from MaKaC
.common
.mail
import GenericMailer
46 from MaKaC
.webinterface
.rh
.base
import RH
49 def _get_provider(name
, external
):
51 provider
= multipass
.auth_providers
[name
]
53 raise NotFound('Provider does not exist')
54 if provider
.is_external
!= external
:
55 raise NotFound('Invalid provider')
63 login_reason
= session
.pop('login_reason', None)
65 # User is already logged in
66 if session
.user
is not None:
67 multipass
.set_next_url()
68 return multipass
.redirect_success()
70 # If we have only one provider, and this provider is external, we go there immediately
71 # However, after a failed login we need to show the page to avoid a redirect loop
72 if not session
.pop('_multipass_auth_failed', False) and 'provider' not in request
.view_args
:
73 single_auth_provider
= multipass
.single_auth_provider
74 if single_auth_provider
and single_auth_provider
.is_external
:
75 multipass
.set_next_url()
76 return redirect(url_for('.login', provider
=single_auth_provider
.name
))
78 # Save the 'next' url to go to after login
79 multipass
.set_next_url()
81 # If there's a provider in the URL we start the external login process
82 if 'provider' in request
.view_args
:
83 provider
= _get_provider(request
.view_args
['provider'], True)
84 return provider
.initiate_external_login()
86 # If we have a POST request we submitted a login form for a local provider
87 if request
.method
== 'POST':
88 active_provider
= provider
= _get_provider(request
.form
['_provider'], False)
89 form
= provider
.login_form()
90 if form
.validate_on_submit():
91 response
= multipass
.handle_login_form(provider
, form
.data
)
94 # Otherwise we show the form for the default provider
96 active_provider
= multipass
.default_local_auth_provider
97 form
= active_provider
.login_form() if active_provider
else None
99 providers
= multipass
.auth_providers
.values()
100 return render_template('auth/login_page.html', form
=form
, providers
=providers
, active_provider
=active_provider
,
101 login_reason
=login_reason
)
104 class RHLoginForm(RH
):
105 """Retrieves a login form (json)"""
108 provider
= _get_provider(request
.view_args
['provider'], False)
109 form
= provider
.login_form()
110 template_module
= get_template_module('auth/_login_form.html')
111 return jsonify(success
=True, html
=template_module
.login_form(provider
, form
))
115 """Logs the user out"""
118 return multipass
.logout(request
.args
.get('next') or url_for('misc.index'), clear_session
=True)
121 def _send_confirmation(email
, salt
, endpoint
, template
, template_args
=None, url_args
=None, data
=None):
122 template_args
= template_args
or {}
123 url_args
= url_args
or {}
124 token
= secure_serializer
.dumps(data
or email
, salt
=salt
)
125 url
= url_for(endpoint
, token
=token
, _external
=True, _secure
=True, **url_args
)
126 template_module
= get_template_module(template
, email
=email
, url
=url
, **template_args
)
127 GenericMailer
.send(make_email(email
, template
=template_module
))
128 flash(_('We have sent you a verification email. Please check your mailbox within the next hour and open '
129 'the link in that email.'))
130 return redirect(url_for(endpoint
, **url_args
))
133 class RHLinkAccount(RH
):
134 """Links a new identity with an existing user.
136 This RH is only used if the identity information contains an
137 email address and an existing user was found.
140 def _checkParams(self
):
141 self
.identity_info
= load_identity_info()
142 if not self
.identity_info
or self
.identity_info
['indico_user_id'] is None:
143 # Just redirect to the front page or whereever we wanted to go.
144 # Probably someone simply used his browser's back button.
145 flash('There is no pending login.', 'warning')
146 return multipass
.redirect_success()
147 self
.user
= User
.get(self
.identity_info
['indico_user_id'])
148 self
.emails
= sorted(self
.user
.all_emails
& set(self
.identity_info
['data'].getlist('email')))
149 self
.verification_email_sent
= self
.identity_info
.get('verification_email_sent', False)
150 self
.email_verified
= self
.identity_info
['email_verified']
151 self
.must_choose_email
= len(self
.emails
) != 1 and not self
.email_verified
154 if self
.verification_email_sent
and 'token' in request
.args
:
155 email
= secure_serializer
.loads(request
.args
['token'], max_age
=3600, salt
='link-identity-email')
156 if email
not in self
.emails
:
157 raise BadData('Emails do not match')
158 session
['login_identity_info']['email_verified'] = True
159 session
.modified
= True
160 flash(_('You have successfully validated your email address and can now proceeed with the login.'),
162 return redirect(url_for('.link_account', provider
=self
.identity_info
['provider']))
164 if self
.must_choose_email
:
165 form
= SelectEmailForm()
166 form
.email
.choices
= zip(self
.emails
, self
.emails
)
170 if form
.validate_on_submit():
171 if self
.email_verified
:
172 return self
._create
_identity
()
173 elif not self
.verification_email_sent
:
174 return self
._send
_confirmation
(form
.email
.data
if self
.must_choose_email
else self
.emails
[0])
176 flash(_('The validation email has already been sent.'), 'warning')
178 return WPAuth
.render_template('link_identity.html', identity_info
=self
.identity_info
, user
=self
.user
,
179 email_sent
=self
.verification_email_sent
, emails
=' / '.join(self
.emails
),
180 form
=form
, must_choose_email
=self
.must_choose_email
)
182 def _create_identity(self
):
183 identity
= Identity(user
=self
.user
, provider
=self
.identity_info
['provider'],
184 identifier
=self
.identity_info
['identifier'], data
=self
.identity_info
['data'],
185 multipass_data
=self
.identity_info
['multipass_data'])
186 logger
.info('Created new identity for {}: {}'.format(self
.user
, identity
))
187 del session
['login_identity_info']
189 login_user(self
.user
, identity
)
190 return multipass
.redirect_success()
192 def _send_confirmation(self
, email
):
193 session
['login_identity_info']['verification_email_sent'] = True
194 session
['login_identity_info']['data']['email'] = email
# throw away other emails
195 return _send_confirmation(email
, 'link-identity-email', '.link_account',
196 'auth/emails/link_identity_verify_email.txt', {'user': self
.user
},
197 url_args
={'provider': self
.identity_info
['provider']})
200 class RHRegister(RH
):
201 """Creates a new indico user.
203 This handles two cases:
204 - creation of a new user with a locally stored username and password
205 - creation of a new user based on information from an identity provider
208 def _checkParams(self
):
209 self
.identity_info
= None
210 self
.provider_name
= request
.view_args
['provider']
211 if self
.provider_name
is not None:
212 self
.identity_info
= info
= load_identity_info()
214 return redirect(url_for('.login'))
215 elif info
['indico_user_id'] is not None or info
['provider'] != self
.provider_name
:
216 # If we have a matching user id, we shouldn't be on the registration page
217 # If the provider doesn't match it would't be a big deal but the request doesn't make sense
219 elif not Config
.getInstance().getLocalIdentities():
220 raise Forbidden('Local identities are disabled')
222 def _get_verified_email(self
):
223 """Checks if there is an email verification token."""
224 if 'token' not in request
.args
:
226 return secure_serializer
.loads(request
.args
['token'], max_age
=3600, salt
='register-email')
230 return redirect(url_for('misc.index'))
231 handler
= MultipassRegistrationHandler(self
) if self
.identity_info
else LocalRegistrationHandler(self
)
232 verified_email
= self
._get
_verified
_email
()
233 if verified_email
is not None:
234 handler
.email_verified(verified_email
)
235 flash(_('You have successfully validated your email address and can now proceeed with the registration.'),
237 return redirect(url_for('.register', provider
=self
.provider_name
))
239 form
= handler
.create_form()
240 # Check for pending users if we have verified emails
242 if not handler
.must_verify_email
:
243 pending
= User
.find_first(~User
.is_deleted
, User
.is_pending
,
244 User
.all_emails
.contains(db
.func
.any(list(handler
.get_all_emails(form
)))))
246 if form
.validate_on_submit():
247 if handler
.must_verify_email
:
248 return self
._send
_confirmation
(form
.email
.data
)
250 return self
._create
_user
(form
, handler
, pending
)
251 elif not form
.is_submitted() and pending
:
252 # If we have a pending user, populate empty fields with data from that user
254 value
= getattr(pending
, field
.short_name
, '')
255 if value
and not field
.data
:
258 flash(_("There is already some information in Indico that concerns you. "
259 "We are going to link it automatically."), 'info')
260 return WPAuth
.render_template('register.html', form
=form
, local
=(not self
.identity_info
),
261 must_verify_email
=handler
.must_verify_email
, widget_attrs
=handler
.widget_attrs
,
262 email_sent
=session
.pop('register_verification_email_sent', False))
264 def _send_confirmation(self
, email
):
265 session
['register_verification_email_sent'] = True
266 return _send_confirmation(email
, 'register-email', '.register', 'auth/emails/register_verify_email.txt',
267 url_args
={'provider': self
.provider_name
})
269 def _create_user(self
, form
, handler
, pending_user
):
273 user
.is_pending
= False
276 form
.populate_obj(user
, skip
={'email'})
277 if form
.email
.data
in user
.secondary_emails
:
278 # This can happen if there's a pending user who has a secondary email
279 # for some weird reason which should now become the primary email...
280 user
.make_email_primary(form
.email
.data
)
282 user
.email
= form
.email
.data
283 identity
= handler
.create_identity(data
)
284 user
.identities
.add(identity
)
285 user
.secondary_emails |
= handler
.get_all_emails(form
) - {user
.email
}
286 user
.favorite_users
.add(user
)
288 minfo
= HelperMaKaCInfo
.getMaKaCInfoInstance()
289 timezone
= session
.timezone
290 if timezone
== 'LOCAL':
291 timezone
= Config
.getInstance().getDefaultTimezone()
292 user
.settings
.set('timezone', timezone
)
293 user
.settings
.set('lang', session
.lang
or minfo
.getLang())
294 handler
.update_user(user
, form
)
297 # notify everyone of user creation
298 signals
.users
.registered
.send(user
)
300 login_user(user
, identity
)
301 msg
= _('You have sucessfully registered your Indico profile. '
302 'Check <a href="{url}">your profile</a> for further details and settings.')
303 flash(Markup(msg
).format(url
=url_for('users.user_profile')), 'success')
305 return handler
.redirect_success()
308 class RHAccounts(RHUserBase
):
309 """Displays user accounts"""
311 def _create_form(self
):
312 if self
.user
.local_identity
:
313 defaults
= FormDefaults(username
=self
.user
.local_identity
.identifier
)
314 local_account_form
= EditLocalIdentityForm(identity
=self
.user
.local_identity
, obj
=defaults
)
316 local_account_form
= AddLocalIdentityForm()
317 return local_account_form
319 def _handle_add_local_account(self
, form
):
320 identity
= Identity(provider
='indico', identifier
=form
.data
['username'], password
=form
.data
['password'])
321 self
.user
.identities
.add(identity
)
322 flash(_("Local account added successfully"), 'success')
324 def _handle_edit_local_account(self
, form
):
325 self
.user
.local_identity
.identifier
= form
.data
['username']
326 if form
.data
['new_password']:
327 self
.user
.local_identity
.password
= form
.data
['new_password']
328 flash(_("Your local account credentials have been updated successfully"), 'success')
331 form
= self
._create
_form
()
332 if form
.validate_on_submit():
333 if isinstance(form
, AddLocalIdentityForm
):
334 self
._handle
_add
_local
_account
(form
)
335 elif isinstance(form
, EditLocalIdentityForm
):
336 self
._handle
_edit
_local
_account
(form
)
337 return redirect(url_for('auth.accounts'))
338 provider_titles
= {name
: provider
.title
for name
, provider
in multipass
.identity_providers
.iteritems()}
339 return WPUser
.render_template('accounts.html', form
=form
, user
=self
.user
, provider_titles
=provider_titles
)
342 class RHRemoveAccount(RHUserBase
):
343 """Removes an identity linked to a user"""
345 def _checkParams(self
):
346 RHUserBase
._checkParams
(self
)
347 self
.identity
= Identity
.get_one(request
.view_args
['identity'])
348 if self
.identity
.user
!= self
.user
:
352 if session
['login_identity'] == self
.identity
.id:
353 raise BadRequest("The identity used to log in can't be removed")
354 if self
.user
.local_identity
== self
.identity
:
355 raise BadRequest("The main local identity can't be removed")
356 self
.user
.identities
.remove(self
.identity
)
357 provider_title
= multipass
.identity_providers
[self
.identity
.provider
].title
358 flash(_("{provider} ({identifier}) successfully removed from your accounts"
359 .format(provider
=provider_title
, identifier
=self
.identity
.identifier
)), 'success')
360 return redirect(url_for('.accounts'))
363 class RegistrationHandler(object):
366 def __init__(self
, rh
):
369 def email_verified(self
, email
):
370 raise NotImplementedError
372 def get_form_defaults(self
):
373 raise NotImplementedError
375 def create_form(self
):
376 defaults
= self
.get_form_defaults()
377 if self
.must_verify_email
:
378 # We don't bother with multiple emails here. The case that the provider sends more
379 # than one email AND those emails are untrusted is so low it's simply not worth it.
380 # The only drawback in that situation would be not showing the extra emails to the
382 return RegistrationEmailForm(obj
=defaults
)
384 return self
.form(obj
=defaults
)
387 def widget_attrs(self
):
391 def must_verify_email(self
):
392 raise NotImplementedError
394 def get_all_emails(self
, form
):
395 # All (verified!) emails that should be set on the user.
396 # This MUST include the primary email from the form if available.
397 # Any additional emails will be set as secondary emails
398 # The emails returned here are used to check for pending users
399 return {form
.email
.data
} if form
.validate_on_submit() else set()
401 def create_identity(self
, data
):
402 raise NotImplementedError
404 def update_user(self
, user
, form
):
407 def redirect_success(self
):
408 raise NotImplementedError
411 class MultipassRegistrationHandler(RegistrationHandler
):
412 def __init__(self
, rh
):
413 self
.identity_info
= rh
.identity_info
416 def from_sync_provider(self
):
417 # If the multipass login came from the provider that's used for synchronization
418 return multipass
.sync_provider
and multipass
.sync_provider
.name
== self
.identity_info
['provider']
420 def email_verified(self
, email
):
421 session
['login_identity_info']['data']['email'] = email
422 session
['login_identity_info']['email_verified'] = True
423 session
.modified
= True
425 def get_form_defaults(self
):
426 return FormDefaults(self
.identity_info
['data'])
428 def create_form(self
):
429 form
= super(MultipassRegistrationHandler
, self
).create_form()
430 # We only want the phone/address fields if the provider gave us data for it
431 for field
in {'address', 'phone'}:
432 if field
in form
and not self
.identity_info
['data'].get(field
):
434 emails
= self
.identity_info
['data'].getlist('email')
435 form
.email
.choices
= zip(emails
, emails
)
438 def form(self
, **kwargs
):
439 if self
.from_sync_provider
:
440 synced_values
= {k
: v
or '' for k
, v
in self
.identity_info
['data'].iteritems()}
441 return MultipassRegistrationForm(synced_fields
=multipass
.synced_fields
, synced_values
=synced_values
,
444 return MultipassRegistrationForm(**kwargs
)
447 def must_verify_email(self
):
448 return not self
.identity_info
['email_verified']
450 def get_all_emails(self
, form
):
451 emails
= super(MultipassRegistrationHandler
, self
).get_all_emails(form
)
452 return emails |
set(self
.identity_info
['data'].getlist('email'))
454 def create_identity(self
, data
):
455 del session
['login_identity_info']
456 return Identity(provider
=self
.identity_info
['provider'], identifier
=self
.identity_info
['identifier'],
457 data
=self
.identity_info
['data'], multipass_data
=self
.identity_info
['multipass_data'])
459 def update_user(self
, user
, form
):
460 if self
.from_sync_provider
:
461 user
.synced_fields
= form
.synced_fields |
{field
for field
in multipass
.synced_fields
if field
not in form
}
463 def redirect_success(self
):
464 return multipass
.redirect_success()
467 class LocalRegistrationHandler(RegistrationHandler
):
468 form
= LocalRegistrationForm
470 def __init__(self
, rh
):
471 if 'next' in request
.args
:
472 session
['register_next_url'] = request
.args
['next']
475 def widget_attrs(self
):
476 return {'email': {'disabled': not self
.must_verify_email
}}
479 def must_verify_email(self
):
480 return 'register_verified_email' not in session
482 def get_all_emails(self
, form
):
483 emails
= super(LocalRegistrationHandler
, self
).get_all_emails(form
)
484 if not self
.must_verify_email
:
485 emails
.add(session
['register_verified_email'])
488 def email_verified(self
, email
):
489 session
['register_verified_email'] = email
491 def get_form_defaults(self
):
492 email
= session
.get('register_verified_email')
493 existing_user_id
= session
.get('register_pending_user')
494 existing_user
= User
.get(existing_user_id
) if existing_user_id
else None
495 data
= {'email': email
}
498 data
.update(first_name
=existing_user
.first_name
,
499 last_name
=existing_user
.last_name
,
500 affiliation
=existing_user
.affiliation
)
502 return FormDefaults(**data
)
504 def create_form(self
):
505 form
= super(LocalRegistrationHandler
, self
).create_form()
506 if not self
.must_verify_email
:
507 form
.email
.data
= session
['register_verified_email']
510 def create_identity(self
, data
):
511 del session
['register_verified_email']
512 return Identity(provider
='indico', identifier
=data
['username'], password
=data
['password'])
514 def redirect_success(self
):
515 return redirect(session
.pop('register_next_url', url_for('misc.index')))
518 class RHResetPassword(RH
):
519 """Resets the password for a local identity."""
521 def _checkParams(self
):
522 if not Config
.getInstance().getLocalIdentities():
523 raise Forbidden('Local identities are disabled')
526 if 'token' in request
.args
:
527 identity_id
= secure_serializer
.loads(request
.args
['token'], max_age
=3600, salt
='reset-password')
528 identity
= Identity
.get(identity_id
)
530 raise BadData('Identity does not exist')
531 return self
._reset
_password
(identity
)
533 return self
._request
_token
()
535 def _request_token(self
):
536 form
= ResetPasswordEmailForm()
537 if form
.validate_on_submit():
539 # The only case where someone would have more than one identity is after a merge.
540 # And the worst case that can happen here is that we send the user a different
541 # username than the one he expects. But he still gets back into his profile.
542 # Showing a list of usernames would be a little bit more user-friendly but less
543 # secure as we'd expose valid usernames for a specific user to an untrusted person.
544 identity
= next(iter(user
.local_identities
))
545 _send_confirmation(form
.email
.data
, 'reset-password', '.resetpass', 'auth/emails/reset_password.txt',
546 {'user': user
, 'username': identity
.identifier
}, data
=identity
.id)
547 session
['resetpass_email_sent'] = True
548 return redirect(url_for('.resetpass'))
549 return WPAuth
.render_template('reset_password.html', form
=form
, identity
=None, widget_attrs
={},
550 email_sent
=session
.pop('resetpass_email_sent', False))
552 def _reset_password(self
, identity
):
553 form
= ResetPasswordForm()
554 if form
.validate_on_submit():
555 identity
.password
= form
.password
.data
556 flash(_("Your password has been changed successfully."), 'success')
557 login_user(identity
.user
, identity
)
558 # We usually come here from a multipass login page so we should have a target url
559 return multipass
.redirect_success()
560 form
.username
.data
= identity
.identifier
561 return WPAuth
.render_template('reset_password.html', form
=form
, identity
=identity
,
562 widget_attrs
={'username': {'disabled': True}})