~canonical-isd-hackers/canonical-identity-provider/sst-changes

« back to all changes in this revision

Viewing changes to identityprovider/views/ui.py

  • Committer: Danny Tamez
  • Date: 2010-04-21 15:29:24 UTC
  • Revision ID: danny.tamez@canonical.com-20100421152924-lq1m92tstk2iz75a
Canonical SSO Provider (Open Source) - Initial Commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2010 Canonical Ltd.  This software is licensed under the
 
2
# GNU Affero General Public License version 3 (see the file LICENSE).
 
3
 
 
4
import logging
 
5
 
 
6
from urlparse import urljoin
 
7
 
 
8
import django.contrib.auth as auth
 
9
 
 
10
from django.conf import settings
 
11
from django.contrib.auth.decorators import login_required
 
12
from django.core.mail import send_mail
 
13
from django.core.urlresolvers import Resolver404, resolve, reverse
 
14
from django.http import Http404, HttpResponseRedirect, HttpResponseNotFound
 
15
from django.shortcuts import (get_object_or_404, get_list_or_404,
 
16
                              render_to_response)
 
17
from django.template import RequestContext
 
18
from django.utils.translation import ugettext as _
 
19
from django.template.loader import render_to_string
 
20
 
 
21
from identityprovider.branding import current_brand
 
22
from identityprovider.decorators import guest_required, dont_cache, limitlogin
 
23
from identityprovider.forms import (LoginForm, ForgotPasswordForm,
 
24
    ResetPasswordForm, NewAccountForm, ConfirmNewAccountForm)
 
25
from identityprovider.models import (Account, AuthToken, AuthTokenFactory,
 
26
    EmailAddress)
 
27
from identityprovider.models.const import EmailStatus, LoginTokenType
 
28
import identityprovider.signed as signed
 
29
from identityprovider.utils import (encrypt_launchpad_password,
 
30
    format_address, get_person_and_account_by_email, polite_form_errors,
 
31
    CannotResetPasswordException, PersonAndAccountNotFoundException)
 
32
from identityprovider.decorators import check_readonly
 
33
from identityprovider.views.utils import get_rpconfig
 
34
from identityprovider.models.captcha import Captcha
 
35
 
 
36
 
 
37
logger = logging.getLogger('sso')
 
38
 
 
39
 
 
40
@guest_required
 
41
@dont_cache
 
42
@limitlogin()
 
43
def login(request, token=None, rpconfig=None):
 
44
    form = LoginForm()
 
45
    if request.method == 'POST':
 
46
        form = LoginForm(request.POST)
 
47
        if form.is_valid():
 
48
            username = form.cleaned_data['email']
 
49
            password = form.cleaned_data['password']
 
50
            user = auth.authenticate(username=username, password=password)
 
51
            auth.login(request, user)
 
52
            next = request.POST.get('next')
 
53
            limitlogin().reset_count(request)
 
54
            if next and _is_safe_redirect_url(next):
 
55
                return HttpResponseRedirect(next)
 
56
            elif token:
 
57
                return HttpResponseRedirect('/%s/' % token)
 
58
            else:
 
59
                return HttpResponseRedirect('/')
 
60
        else:
 
61
            polite_form_errors(form._errors)
 
62
    context = RequestContext(request, {
 
63
        'form': form,
 
64
        'next': request.GET.get('next'),
 
65
        'rpconfig': rpconfig,
 
66
        'token': token is not None,
 
67
        'message': request.session.get('message', None),
 
68
        'message_style': 'informational',
 
69
    })
 
70
    try:
 
71
        del request.session['message']
 
72
    except:
 
73
        pass
 
74
    return render_to_response('registration/login.html', context)
 
75
 
 
76
 
 
77
def _is_safe_redirect_url(url):
 
78
    """ Check whether 'url' resolves into a valid URL this application can
 
79
    serve. """
 
80
    try:
 
81
        if url.find('?') >= 0:
 
82
            url = url[:url.find('?')]
 
83
        response = resolve(url)
 
84
    except Resolver404:
 
85
        return False
 
86
    if response is None:
 
87
        return False
 
88
    view, args, kwargs = response
 
89
    return True
 
90
 
 
91
 
 
92
def logout(request, token=None):
 
93
    # We don't want to lose session[token] when we log the user out
 
94
    raw_orequest = request.session.get(token, None)
 
95
    auth.logout(request)
 
96
    if token is not None and raw_orequest is not None:
 
97
        request.session[token] = raw_orequest
 
98
    request.session['message'] = _("You have successfully logged out")
 
99
    return HttpResponseRedirect('+login')
 
100
 
 
101
 
 
102
def claim_token(request, authtoken):
 
103
    atrequest = get_object_or_404(AuthToken, token=authtoken)
 
104
    if atrequest.token_type == LoginTokenType.PASSWORDRECOVERY:
 
105
        return HttpResponseRedirect('/token/%s/+resetpassword' % authtoken)
 
106
    elif atrequest.token_type == LoginTokenType.NEWPERSONLESSACCOUNT:
 
107
        return HttpResponseRedirect('/token/%s/+newaccount' % authtoken)
 
108
    elif atrequest.token_type == LoginTokenType.VALIDATEEMAIL:
 
109
        return HttpResponseRedirect('/token/%s/+newemail' % authtoken)
 
110
    else:
 
111
        raise Http404()
 
112
 
 
113
 
 
114
@guest_required
 
115
@check_readonly
 
116
def new_account(request, token=None):
 
117
    form = NewAccountForm()
 
118
    if request.method == 'POST':
 
119
        form = NewAccountForm(request.POST)
 
120
        if form.is_valid():
 
121
 
 
122
            response = _verify_captcha_response(
 
123
                'registration/new_account.html', request, form)
 
124
            if response:
 
125
                return response
 
126
 
 
127
            email = form.cleaned_data['email']
 
128
            already_exists = True
 
129
            try:
 
130
                person, account = get_person_and_account_by_email(email)
 
131
            except (CannotResetPasswordException,
 
132
                    PersonAndAccountNotFoundException):
 
133
                already_exists = False
 
134
            if already_exists:
 
135
                if account is not None and account.is_active:
 
136
                    # only send out email if account is active
 
137
                    # otherwise disabled account can be spammed
 
138
                    replacements = {
 
139
                        'forgotten_password_url':
 
140
                        urljoin(settings.SSO_ROOT_URL, '+forgot_password'),
 
141
                        'sso_brand_name': current_brand.name,
 
142
                    }
 
143
                    message = render_to_string(
 
144
                        'email/impersonate-warning.txt', replacements)
 
145
                    subject = current_brand.name + ": " + _("Warning")
 
146
                    from_address = format_address(
 
147
                        settings.NOREPLY_FROM_ADDRESS, current_brand.name)
 
148
                    send_mail(subject, message, from_address, [email])
 
149
                else:
 
150
                    # log why email was not sent
 
151
                    if account is None:
 
152
                        condition = ("account for person '%s' does not exist" %
 
153
                                     person.displayname)
 
154
                    else:
 
155
                        condition = ("account '%s' is not active" %
 
156
                                     account.displayname)
 
157
                    logger.debug("In view 'new_account' email was not sent "
 
158
                                 "out because %s" % condition)
 
159
            else:
 
160
                view, args, kwargs = resolve(request.get_full_path())
 
161
                if token is not None:
 
162
                    # Redirected from an OpenID request, must get back after
 
163
                    # confirmation
 
164
                    redirection_url = '/%s/+decide' % token
 
165
                else:
 
166
                    redirection_url = '/'
 
167
                token = AuthTokenFactory().new(
 
168
                    requester=None, requester_email=None, email=email,
 
169
                    token_type=LoginTokenType.NEWPERSONLESSACCOUNT,
 
170
                    redirection_url=redirection_url)
 
171
                token.sendNewUserEmail()
 
172
                request.session['token_newaccount'] = token.token
 
173
            request.session['email_feedback'] = settings.FEEDBACK_TO_ADDRESS
 
174
            request.session['email_heading'] = _("Registration mail sent")
 
175
            request.session['email_reason'] = _("We’ve just emailed "
 
176
                "%(email_to)s (from %(email_from)s) to confirm "
 
177
                "your address.") % {
 
178
                    'email_to': email,
 
179
                    'email_from': settings.NOREPLY_FROM_ADDRESS,
 
180
            }
 
181
            request.session['email_notreceived_extra'] = None
 
182
            return HttpResponseRedirect('/+email-sent')
 
183
        else:
 
184
            polite_form_errors(form._errors)
 
185
 
 
186
    return render_to_response('registration/new_account.html',
 
187
                    RequestContext(request, {'form': form,
 
188
                     'CAPTCHA_PUBLIC_KEY': settings.CAPTCHA_PUBLIC_KEY,
 
189
                     }))
 
190
 
 
191
 
 
192
def _verify_captcha_response(template, request, form):
 
193
    captcha = Captcha(request.environ,
 
194
                      request.POST.get('recaptcha_challenge_field'))
 
195
    captcha_solution = request.POST.get('recaptcha_response_field')
 
196
    verified = captcha.verify(captcha_solution)
 
197
    if not verified:
 
198
        return render_to_response(template,
 
199
                RequestContext(request, {'form': form,
 
200
                 'CAPTCHA_PUBLIC_KEY': settings.CAPTCHA_PUBLIC_KEY,
 
201
                 'captcha_error': ('&error=%s' % captcha.message),
 
202
                 }))
 
203
    return None
 
204
 
 
205
 
 
206
@guest_required
 
207
@check_readonly
 
208
def confirm_account(request, authtoken):
 
209
    atrequest = get_object_or_404(AuthToken, token=authtoken,
 
210
        token_type=LoginTokenType.NEWPERSONLESSACCOUNT)
 
211
    session_token = request.session.get('token_newaccount')
 
212
    if session_token is None or session_token != atrequest.token:
 
213
        return HttpResponseRedirect('/+bad-token')
 
214
    form = ConfirmNewAccountForm()
 
215
    if request.method == 'POST':
 
216
        form = ConfirmNewAccountForm(request.POST)
 
217
        if form.is_valid():
 
218
            username = atrequest.email
 
219
            password = form.cleaned_data['password']
 
220
            displayname = form.cleaned_data['displayname']
 
221
            creation_rationale = None
 
222
            if atrequest.redirection_url.endswith('+decide'):
 
223
                # Try to determine creation rationale from RP config
 
224
                view, args, kwargs = resolve(atrequest.redirection_url)
 
225
                if 'token' in kwargs:
 
226
                    try:
 
227
                        raw_orequest = request.session.get(
 
228
                            kwargs['token'], None)
 
229
                        openid_request = signed.loads(
 
230
                            raw_orequest, settings.SECRET_KEY)
 
231
 
 
232
                        rpconfig = get_rpconfig(openid_request.trust_root)
 
233
                        if rpconfig is not None:
 
234
                            creation_rationale = rpconfig.creation_rationale
 
235
                    except:
 
236
                        pass
 
237
            Account.objects.create_account(
 
238
                displayname, username, password, creation_rationale)
 
239
            user = auth.authenticate(username=username, password=password)
 
240
            auth.login(request, user)
 
241
            atrequest.consume()
 
242
            request.session['message'] = _("Your account was created "\
 
243
                "successfully")
 
244
            return HttpResponseRedirect(atrequest.redirection_url)
 
245
    return render_to_response('registration/confirm_new_account.html',
 
246
        RequestContext(request, {'form': form}))
 
247
 
 
248
 
 
249
@login_required
 
250
def confirm_email(request, authtoken):
 
251
    atrequest = get_object_or_404(AuthToken, token=authtoken,
 
252
        token_type=LoginTokenType.VALIDATEEMAIL)
 
253
    email = get_object_or_404(EmailAddress, email__iexact=atrequest.email)
 
254
 
 
255
    if request.user.id != email.account.id:
 
256
        atrequest.delete()
 
257
        raise Http404
 
258
 
 
259
    if request.method == 'POST':
 
260
        # only confirm the email address if the form was submitted
 
261
        email.status = EmailStatus.VALIDATED
 
262
        email.save()
 
263
        atrequest.consume()
 
264
        request.session['message'] = _("The email address %s has been "
 
265
            "validated" % email)
 
266
        return HttpResponseRedirect(atrequest.redirection_url)
 
267
 
 
268
    # form was not submitted
 
269
    return render_to_response('account/confirm_new_email.html',
 
270
        RequestContext(request, {'email': email.email}))
 
271
 
 
272
 
 
273
def token_from_email(request, email):
 
274
    if getattr(settings, 'ENABLE_TOKEN_DEBUG', False):
 
275
        tokens = get_list_or_404(AuthToken, email=email)
 
276
        return render_to_response('registration/token.html',
 
277
            RequestContext(request, {'token': tokens[0].token}))
 
278
    else:
 
279
        return HttpResponseNotFound()
 
280
 
 
281
 
 
282
def email_sent(request, token=None):
 
283
    context = RequestContext(request, {
 
284
        'email_feedback': request.session.get('email_feedback'),
 
285
        'email_heading': request.session.get('email_heading'),
 
286
        'email_reason': request.session.get('email_reason'),
 
287
        'email_notreceived_extra': request.session.get(
 
288
            'email_notreceived_extra'),
 
289
        'token': token,
 
290
    })
 
291
 
 
292
    return render_to_response('registration/email_sent.html', context)
 
293
 
 
294
 
 
295
def bad_token(request):
 
296
    context = RequestContext(request, {
 
297
        'email_feedback': settings.FEEDBACK_TO_ADDRESS,
 
298
    })
 
299
    return render_to_response('registration/bad_token.html', context)
 
300
 
 
301
 
 
302
def deactivated(request):
 
303
    context = RequestContext(request, {
 
304
        'message': request.session.pop('message', None),
 
305
        'message_style': 'informational'
 
306
    })
 
307
    return render_to_response('account/deactivated.html', context)
 
308
 
 
309
 
 
310
@guest_required
 
311
@check_readonly
 
312
def forgot_password(request, token=None):
 
313
    form = ForgotPasswordForm()
 
314
    if request.method == 'POST':
 
315
        form = ForgotPasswordForm(request.POST)
 
316
        if form.is_valid():
 
317
            response = _verify_captcha_response(
 
318
                'registration/forgot_password.html', request, form)
 
319
            if response:
 
320
                return response
 
321
            email = form.cleaned_data['email']
 
322
            send_email = True
 
323
            try:
 
324
                person, account = get_person_and_account_by_email(email)
 
325
                if account is not None and not account.is_active:
 
326
                    send_email = False
 
327
                    # log why email was not sent
 
328
                    condition = ("account '%s' is not active" %
 
329
                                 account.displayname)
 
330
                    logger.debug("In view 'forgot_password' email was not "
 
331
                                 "sent out because %s" % condition)
 
332
            except (CannotResetPasswordException,
 
333
                    PersonAndAccountNotFoundException):
 
334
                send_email = False
 
335
            view, args, kwargs = resolve(request.get_full_path())
 
336
            if send_email:
 
337
                if token is not None:
 
338
                    # Redirected from an OpenID request, must get back after
 
339
                    # confirmation
 
340
                    redirection_url = reverse(
 
341
                        'identityprovider.views.server.decide',
 
342
                        kwargs={'token': token})
 
343
                else:
 
344
                    redirection_url = '/'
 
345
                token = AuthTokenFactory().new(
 
346
                    account, email, email, LoginTokenType.PASSWORDRECOVERY,
 
347
                    redirection_url=redirection_url)
 
348
                token.sendPasswordResetEmail()
 
349
                request.session['token_forgotpassword'] = token.token
 
350
            request.session['email_feedback'] = settings.FEEDBACK_TO_ADDRESS
 
351
            request.session['email_heading'] = _("Forgotten your password?")
 
352
            request.session['email_reason'] = _("We’ve just emailed "
 
353
                "%(email_to)s (from %(email_from)s) with "
 
354
                "instructions on resetting your password.") % {
 
355
                    'email_to': email,
 
356
                    'email_from': settings.NOREPLY_FROM_ADDRESS,
 
357
            }
 
358
            request.session['email_notreceived_extra'] = _("Check that "
 
359
                "you’ve actually entered a subscribed email address.")
 
360
            return HttpResponseRedirect('/+email-sent')
 
361
        else:
 
362
            polite_form_errors(form._errors)
 
363
 
 
364
    return render_to_response('registration/forgot_password.html',
 
365
            RequestContext(request, {
 
366
                'form': form,
 
367
                'CAPTCHA_PUBLIC_KEY': settings.CAPTCHA_PUBLIC_KEY,
 
368
                }))
 
369
 
 
370
 
 
371
@guest_required
 
372
def reset_password(request, authtoken):
 
373
    atrequest = get_object_or_404(AuthToken, token=authtoken,
 
374
        token_type=LoginTokenType.PASSWORDRECOVERY)
 
375
    account = atrequest.requester
 
376
    session_token = request.session.get('token_forgotpassword')
 
377
    if (session_token is None or session_token != atrequest.token or
 
378
        not account.is_active):
 
379
        # we hide the fact the the account is inactive, to avoid
 
380
        # exposing valid accounts
 
381
        return HttpResponseRedirect('/+bad-token')
 
382
    if request.method == 'POST':
 
383
        form = ResetPasswordForm(request.POST)
 
384
        if form.is_valid():
 
385
            password = form.cleaned_data['password']
 
386
            if account.preferredemail is None:
 
387
                request.session['message'] = _(
 
388
                    'You cannot reset the password of a deactivated account.')
 
389
                return HttpResponseRedirect('/+deactivated')
 
390
            email = account.preferredemail.email
 
391
            password_obj = account.accountpassword
 
392
            password_obj.password = encrypt_launchpad_password(password)
 
393
            password_obj.save()
 
394
            user = auth.authenticate(username=email, password=password)
 
395
            auth.login(request, user)
 
396
            atrequest.consume()
 
397
            return HttpResponseRedirect(atrequest.redirection_url)
 
398
    else:
 
399
        form = ResetPasswordForm()
 
400
    return render_to_response('registration/reset_password.html',
 
401
        RequestContext(request, {'form': form}))
 
402
 
 
403
 
 
404
def static_page(request, page_name):
 
405
    return render_to_response('static/%s.html' % page_name,
 
406
        RequestContext(request))