Package madrona :: Package openid :: Module views
[hide private]

Source Code for Module madrona.openid.views

  1  # -*- coding: utf-8 -*- 
  2  # Copyright 2007, 2008,2009 by Benoît Chesneau <benoitc@e-engura.org> 
  3  #  
  4  # Licensed under the Apache License, Version 2.0 (the "License"); 
  5  # you may not use this file except in compliance with the License. 
  6  # You may obtain a copy of the License at 
  7  # 
  8  #     http://www.apache.org/licenses/LICENSE-2.0 
  9  # 
 10  # Unless required by applicable law or agreed to in writing, software 
 11  # distributed under the License is distributed on an "AS IS" BASIS, 
 12  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 13  # See the License for the specific language governing permissions and 
 14  # limitations under the License. 
 15  # 
 16  from django.conf import settings 
 17  from django.contrib.auth import REDIRECT_FIELD_NAME 
 18  from django.contrib.auth.forms import * 
 19  from django.contrib.auth.models import User 
 20  from django.contrib.auth import login, logout 
 21  from django.contrib.auth.decorators import login_required 
 22  from django.contrib.sites.models import Site 
 23   
 24  from django.http import HttpResponseRedirect 
 25  from django.shortcuts import render_to_response as render 
 26  from django.template import RequestContext, loader, Context 
 27   
 28  from django.core.urlresolvers import reverse 
 29  from django.utils.encoding import smart_unicode 
 30  from django.utils.translation import ugettext as _ 
 31   
 32  from django.utils.http import urlquote_plus 
 33  from django.core.mail import send_mail 
 34   
 35  from openid.consumer.consumer import Consumer, \ 
 36      SUCCESS, CANCEL, FAILURE, SETUP_NEEDED 
 37  from openid.consumer.discover import DiscoveryFailure 
 38  from openid.extensions import sreg, ax 
 39  # needed for some linux distributions like debian 
 40  try: 
 41      from openid.yadis import xri 
 42  except ImportError: 
 43      from yadis import xri 
 44   
 45  import re 
 46  import urllib 
 47   
 48  from madrona.openid import DjangoOpenIDStore 
 49  from madrona.openid.forms import * 
 50  from madrona.openid.models import UserAssociation 
 51  from madrona.openid.signals import oid_register 
 52  from madrona.openid.utils import * 
53 54 -def _build_context(request, extra_context=None):
55 if extra_context is None: 56 extra_context = {} 57 context = RequestContext(request) 58 for key, value in extra_context.items(): 59 context[key] = callable(value) and value() or value 60 return context
61
62 -def ask_openid(request, openid_url, redirect_to, on_failure=None):
63 """ basic function to ask openid and return response """ 64 on_failure = on_failure or signin_failure 65 sreg_req = None 66 ax_req = None 67 68 trust_root = getattr( 69 settings, 'OPENID_TRUST_ROOT', get_url_host(request) + '/' 70 ) 71 if xri.identifierScheme(openid_url) == 'XRI' and getattr( 72 settings, 'OPENID_DISALLOW_INAMES', False 73 ): 74 msg = _("i-names are not supported") 75 return on_failure(request, msg) 76 consumer = Consumer(request.session, DjangoOpenIDStore()) 77 try: 78 auth_request = consumer.begin(openid_url) 79 except DiscoveryFailure: 80 msg = _("The OpenID %s was invalid") % openid_url 81 return on_failure(request, msg) 82 83 # get capabilities 84 use_ax, use_sreg = discover_extensions(openid_url) 85 if use_sreg: 86 # set sreg extension 87 # we always ask for nickname and email 88 sreg_attrs = getattr(settings, 'OPENID_SREG', {}) 89 sreg_attrs.update({"optional": ['nickname', 'email']}) 90 sreg_req = sreg.SRegRequest(**sreg_attrs) 91 if use_ax: 92 # set ax extension 93 # we always ask for nickname and email 94 ax_req = ax.FetchRequest() 95 ax_req.add(ax.AttrInfo('http://schema.openid.net/contact/email', 96 alias='email', required=True)) 97 ax_req.add(ax.AttrInfo('http://schema.openid.net/namePerson/friendly', 98 alias='nickname', required=True)) 99 100 # add custom ax attrs 101 ax_attrs = getattr(settings, 'OPENID_AX', []) 102 for attr in ax_attrs: 103 if len(attr) == 2: 104 ax_req.add(ax.AttrInfo(attr[0], required=alias[1])) 105 else: 106 ax_req.add(ax.AttrInfo(attr[0])) 107 108 if sreg_req is not None: 109 auth_request.addExtension(sreg_req) 110 if ax_req is not None: 111 auth_request.addExtension(ax_req) 112 113 redirect_url = auth_request.redirectURL(trust_root, redirect_to) 114 return HttpResponseRedirect(redirect_url)
115
116 -def complete(request, on_success=None, on_failure=None, return_to=None, 117 **kwargs):
118 """ complete openid signin """ 119 on_success = on_success or default_on_success 120 on_failure = on_failure or default_on_failure 121 122 consumer = Consumer(request.session, DjangoOpenIDStore()) 123 # make sure params are encoded in utf8 124 params = dict((k,smart_unicode(v)) for k, v in request.GET.items()) 125 openid_response = consumer.complete(params, return_to) 126 127 if openid_response.status == SUCCESS: 128 return on_success(request, openid_response.identity_url, 129 openid_response, **kwargs) 130 elif openid_response.status == CANCEL: 131 return on_failure(request, 'The request was canceled', **kwargs) 132 elif openid_response.status == FAILURE: 133 return on_failure(request, openid_response.message, **kwargs) 134 elif openid_response.status == SETUP_NEEDED: 135 return on_failure(request, 'Setup needed', **kwargs) 136 else: 137 assert False, "Bad openid status: %s" % openid_response.status
138
139 -def default_on_success(request, identity_url, openid_response, **kwargs):
140 """ default action on openid signin success """ 141 request.session['openid'] = from_openid_response(openid_response) 142 return HttpResponseRedirect(clean_next(request.GET.get('next')))
143
144 -def default_on_failure(request, message, **kwargs):
145 """ default failure action on signin """ 146 return render('openid_failure.html', { 147 'message': message 148 })
149
150 -def not_authenticated(func):
151 """ decorator that redirect user to next page if 152 he is already logged.""" 153 def decorated(request, *args, **kwargs): 154 if request.user.is_authenticated(): 155 next = request.GET.get("next", "/") 156 return HttpResponseRedirect(next) 157 return func(request, *args, **kwargs)
158 return decorated 159
160 -def signin_success(request, identity_url, openid_response, 161 redirect_field_name=REDIRECT_FIELD_NAME, **kwargs):
162 """ 163 openid signin success. 164 165 If the openid is already registered, the user is redirected to 166 url set par next or in settings with OPENID_REDIRECT_NEXT variable. 167 If none of these urls are set user is redirectd to /. 168 169 if openid isn't registered user is redirected to register page. 170 """ 171 172 openid_ = from_openid_response(openid_response) 173 174 openids = request.session.get('openids', []) 175 openids.append(openid_) 176 request.session['openids'] = openids 177 request.session['openid'] = openid_ 178 try: 179 rel = UserAssociation.objects.get(openid_url__exact=str(openid_)) 180 except: 181 # try to register this new user 182 redirect_to = request.REQUEST.get(redirect_field_name, '') 183 if not redirect_to or '//' in redirect_to or ' ' in redirect_to: 184 redirect_to = settings.LOGIN_REDIRECT_URL 185 return HttpResponseRedirect( 186 "%s?%s" % (reverse('user_register'), 187 urllib.urlencode({redirect_field_name: redirect_to})) 188 ) 189 user_ = rel.user 190 if user_.is_active: 191 user_.backend = "django.contrib.auth.backends.ModelBackend" 192 login(request, user_) 193 194 redirect_to = request.GET.get(redirect_field_name, '') 195 if not redirect_to or '//' in redirect_to or ' ' in redirect_to: 196 redirect_to = settings.LOGIN_REDIRECT_URL 197 return HttpResponseRedirect(redirect_to)
198
199 -def signin_failure(request, message, template_name='authopenid/signin.html', 200 redirect_field_name=REDIRECT_FIELD_NAME, openid_form=OpenidSigninForm, 201 auth_form=AuthenticationForm, extra_context=None, **kwargs):
202 """ 203 falure with openid signin. Go back to signin page. 204 205 :attr request: request object 206 :attr template_name: string, name of template to use, default is 207 'authopenid/signin.html' 208 :attr redirect_field_name: string, field name used for redirect. by default 209 'next' 210 :attr openid_form: form use for openid signin, by default `OpenidSigninForm` 211 :attr auth_form: form object used for legacy authentification. 212 by default AuthentificationForm form auser auth contrib. 213 :attr extra_context: A dictionary of variables to add to the template 214 context. Any callable object in this dictionary will be called to produce 215 the end result which appears in the context. 216 """ 217 return render(template_name, { 218 'msg': message, 219 'form1': openid_form(), 220 'form2': auth_form(), 221 redirect_field_name: request.REQUEST.get(redirect_field_name, '') 222 }, context_instance=_build_context(request, extra_context))
223
224 @not_authenticated 225 -def signin(request, template_name='authopenid/signin.html', 226 redirect_field_name=REDIRECT_FIELD_NAME, openid_form=OpenidSigninForm, 227 auth_form=AuthenticationForm, on_failure=None, extra_context=None):
228 """Signin page. It manage the legacy authentification (user/password) 229 and authentification with openid. 230 231 :attr request: request object 232 :attr template_name: string, name of template to use 233 :attr redirect_field_name: string, field name used for redirect. by 234 default 'next' 235 :attr openid_form: form use for openid signin, by default 236 `OpenidSigninForm` 237 :attr auth_form: form object used for legacy authentification. 238 By default AuthentificationForm form auser auth contrib. 239 :attr extra_context: A dictionary of variables to add to the 240 template context. Any callable object in this dictionary will 241 be called to produce the end result which appears in the context. 242 """ 243 if on_failure is None: 244 on_failure = signin_failure 245 246 from madrona.common.utils import get_logger 247 log = get_logger() 248 249 redirect_to = request.REQUEST.get(redirect_field_name, '') 250 form1 = openid_form() 251 form2 = auth_form() 252 log.debug(request.POST.keys()) 253 if request.POST: 254 if not redirect_to or '//' in redirect_to or ' ' in redirect_to: 255 redirect_to = settings.LOGIN_REDIRECT_URL 256 if 'openid_url' in request.POST.keys(): 257 form1 = openid_form(data=request.POST) 258 if form1.is_valid(): 259 redirect_url = "%s%s?%s" % ( 260 get_url_host(request), 261 reverse('user_complete_signin'), 262 urllib.urlencode({redirect_field_name: redirect_to}) 263 ) 264 return ask_openid(request, 265 form1.cleaned_data['openid_url'], 266 redirect_url, 267 on_failure=on_failure) 268 else: 269 # perform normal django authentification 270 form2 = auth_form(data=request.POST) 271 if form2.is_valid(): 272 login(request, form2.get_user()) 273 if request.session.test_cookie_worked(): 274 request.session.delete_test_cookie() 275 return HttpResponseRedirect(redirect_to) 276 return render(template_name, { 277 'form1': form1, 278 'form2': form2, 279 redirect_field_name: redirect_to, 280 'msg': request.GET.get('msg','') 281 }, context_instance=_build_context(request, extra_context=extra_context))
282
283 -def complete_signin(request, redirect_field_name=REDIRECT_FIELD_NAME, 284 openid_form=OpenidSigninForm, auth_form=AuthenticationForm, 285 on_success=signin_success, on_failure=signin_failure, 286 extra_context=None):
287 """ 288 in case of complete signin with openid 289 290 :attr request: request object 291 :attr openid_form: form use for openid signin, by default 292 `OpenidSigninForm` 293 :attr auth_form: form object used for legacy authentification. 294 by default AuthentificationForm form auser auth contrib. 295 :attr on_success: callbale, function used when openid auth success 296 :attr on_failure: callable, function used when openid auth failed. 297 :attr extra_context: A dictionary of variables to add to the template 298 context. 299 Any callable object in this dictionary will be called to produce the 300 end result which appears in the context. 301 """ 302 return complete(request, on_success, on_failure, 303 get_url_host(request) + reverse('user_complete_signin'), 304 redirect_field_name=redirect_field_name, openid_form=openid_form, 305 auth_form=auth_form, extra_context=extra_context)
306
307 -def is_association_exist(openid_url):
308 """ test if an openid is already in database """ 309 is_exist = True 310 try: 311 uassoc = UserAssociation.objects.get(openid_url__exact=str(openid_url)) 312 except: 313 is_exist = False 314 return is_exist
315
316 -def register_account(form, _openid):
317 """ create an account """ 318 user = User.objects.create_user(form.cleaned_data['username'], 319 form.cleaned_data['email']) 320 user.backend = "django.contrib.auth.backends.ModelBackend" 321 oid_register.send(sender=user, openid=_openid) 322 return user
323
324 @not_authenticated 325 -def register(request, template_name='authopenid/complete.html', 326 redirect_field_name=REDIRECT_FIELD_NAME, 327 register_form=OpenidRegisterForm, auth_form=AuthenticationForm, 328 register_account=register_account, send_email=True, 329 extra_context=None):
330 """ 331 register an openid. 332 333 If user is already a member he can associate its openid with 334 its account. 335 336 A new account could also be created and automaticaly associated 337 to the openid. 338 339 :attr request: request object 340 :attr template_name: string, name of template to use, 341 'authopenid/complete.html' by default 342 :attr redirect_field_name: string, field name used for redirect. by default 343 'next' 344 :attr register_form: form use to create a new account. By default 345 `OpenidRegisterForm` 346 :attr auth_form: form object used for legacy authentification. 347 by default `OpenidVerifyForm` form auser auth contrib. 348 :attr register_account: callback used to create a new account from openid. 349 It take the register_form as param. 350 :attr send_email: boolean, by default True. If True, an email will be sent 351 to the user. 352 :attr extra_context: A dictionary of variables to add to the template 353 context. Any callable object in this dictionary will be called to produce 354 the end result which appears in the context. 355 """ 356 is_redirect = False 357 redirect_to = request.REQUEST.get(redirect_field_name, '') 358 openid_ = request.session.get('openid', None) 359 if openid_ is None or not openid_: 360 return HttpResponseRedirect("%s?%s" % (reverse('user_signin'), 361 urllib.urlencode({ 362 redirect_field_name: redirect_to}))) 363 364 nickname = '' 365 email = '' 366 if openid_.sreg is not None: 367 nickname = openid_.sreg.get('nickname', '') 368 email = openid_.sreg.get('email', '') 369 if openid_.ax is not None and not nickname or not email: 370 if openid_.ax.get('http://schema.openid.net/namePerson/friendly', False): 371 nickname = openid_.ax.get('http://schema.openid.net/namePerson/friendly')[0] 372 if openid_.ax.get('http://schema.openid.net/contact/email', False): 373 email = openid_.ax.get('http://schema.openid.net/contact/email')[0] 374 375 form1 = register_form(initial={ 376 'username': nickname, 377 'email': email, 378 }) 379 form2 = auth_form(initial={ 380 'username': nickname, 381 }) 382 383 if request.POST: 384 user_ = None 385 if not redirect_to or '//' in redirect_to or ' ' in redirect_to: 386 redirect_to = settings.LOGIN_REDIRECT_URL 387 if 'email' in request.POST.keys(): 388 form1 = register_form(data=request.POST) 389 if form1.is_valid(): 390 user_ = register_account(form1, openid_) 391 else: 392 form2 = auth_form(data=request.POST) 393 if form2.is_valid(): 394 user_ = form2.get_user() 395 if user_ is not None: 396 # associate the user to openid 397 uassoc = UserAssociation( 398 openid_url=str(openid_), 399 user_id=user_.id 400 ) 401 uassoc.save(send_email=send_email) 402 login(request, user_) 403 return HttpResponseRedirect(redirect_to) 404 405 return render(template_name, { 406 'form1': form1, 407 'form2': form2, 408 redirect_field_name: redirect_to, 409 'nickname': nickname, 410 'email': email 411 }, context_instance=_build_context(request, extra_context=extra_context))
412
413 @login_required 414 -def signout(request, next_page=None, template_name='registration/logged_out.html'):
415 """ 416 signout from the website. Remove openid from session and kill it. 417 :attr request: request object 418 :attr next_page: default redirect page after logout 419 :attr template_name: string, name of template to use when next_page isn't set, 420 'registration/logged_out.html' by default 421 """ 422 try: 423 del request.session['openid'] 424 except KeyError: 425 pass 426 next = request.GET.get('next') 427 logout(request) 428 if next is not None: 429 return HttpResponseRedirect(next) 430 431 if next_page is None: 432 return render(template_name, { 433 'title': _('Logged out')}, context_instance=RequestContext(request)) 434 435 return HttpResponseRedirect(next_page or request.path)
436
437 -def xrdf(request, template_name='authopenid/yadis.xrdf'):
438 """ view used to process the xrdf file""" 439 440 url_host = get_url_host(request) 441 return_to = [ 442 "%s%s" % (url_host, reverse('user_complete_signin')) 443 ] 444 response = render(template_name, { 445 'return_to': return_to 446 }, context_instance=RequestContext(request)) 447 448 response['Content-Type'] = "application/xrds+xml" 449 response['X-XRDS-Location'] = request.build_absolute_uri(reverse('oid_xrdf')) 450 return response
451
452 @login_required 453 -def password_change(request, 454 template_name='authopenid/password_change_form.html', 455 set_password_form=SetPasswordForm, 456 change_password_form=PasswordChangeForm, post_change_redirect=None, 457 extra_context=None):
458 """ 459 View that allow a user to add a password to its account or change it. 460 461 :attr request: request object 462 :attr template_name: string, name of template to use, 463 'authopenid/password_change_form.html' by default 464 :attr set_password_form: form use to create a new password. By default 465 ``django.contrib.auth.forms.SetPasswordForm`` 466 :attr change_password_form: form objectto change passworf. 467 by default `django.contrib.auth.forms.SetPasswordForm.PasswordChangeForm` 468 form auser auth contrib. 469 :attr post_change_redirect: url used to redirect user after password change. 470 It take the register_form as param. 471 :attr extra_context: A dictionary of variables to add to the template context. 472 Any callable object in this dictionary will be called to produce the 473 end result which appears in the context. 474 """ 475 if post_change_redirect is None: 476 post_change_redirect = settings.LOGIN_REDIRECT_URL 477 478 set_password = False 479 if request.user.has_usable_password(): 480 change_form = change_password_form 481 else: 482 set_password = True 483 change_form = set_password_form 484 485 if request.POST: 486 form = change_form(request.user, request.POST) 487 if form.is_valid(): 488 form.save() 489 msg = urllib.quote(_("Password changed")) 490 redirect_to = "%s?%s" % (post_change_redirect, 491 urllib.urlencode({"msg": msg})) 492 return HttpResponseRedirect(redirect_to) 493 else: 494 form = change_form(request.user) 495 496 return render(template_name, { 497 'form': form, 498 'set_password': set_password 499 }, context_instance=_build_context(request, extra_context=extra_context))
500
501 @login_required 502 -def associate_failure(request, message, 503 template_failure="authopenid/associate.html", 504 openid_form=AssociateOpenID, redirect_name=None, 505 extra_context=None, **kwargs):
506 507 """ function used when new openid association fail""" 508 509 return render(template_failure, { 510 'form': openid_form(request.user), 511 'msg': message, 512 }, context_instance=_build_context(request, extra_context=extra_context))
513
514 @login_required 515 -def associate_success(request, identity_url, openid_response, 516 redirect_field_name=REDIRECT_FIELD_NAME, send_email=True, **kwargs):
517 """ 518 function used when new openid association success. redirect the user 519 """ 520 openid_ = from_openid_response(openid_response) 521 openids = request.session.get('openids', []) 522 openids.append(openid_) 523 request.session['openids'] = openids 524 uassoc = UserAssociation( 525 openid_url=str(openid_), 526 user_id=request.user.id 527 ) 528 uassoc.save(send_email=send_email) 529 530 redirect_to = request.GET.get(redirect_field_name, '') 531 if not redirect_to or '//' in redirect_to or ' ' in redirect_to: 532 redirect_to = settings.LOGIN_REDIRECT_URL 533 return HttpResponseRedirect(redirect_to)
534
535 @login_required 536 -def complete_associate(request, redirect_field_name=REDIRECT_FIELD_NAME, 537 template_failure='authopenid/associate.html', 538 openid_form=AssociateOpenID, redirect_name=None, 539 on_success=associate_success, on_failure=associate_failure, 540 send_email=True, extra_context=None):
541 542 """ in case of complete association with openid """ 543 544 return complete(request, on_success, on_failure, 545 get_url_host(request) + reverse('user_complete_associate'), 546 redirect_field_name=redirect_field_name, openid_form=openid_form, 547 template_failure=template_failure, redirect_name=redirect_name, 548 send_email=send_email, extra_context=extra_context)
549
550 @login_required 551 -def associate(request, template_name='authopenid/associate.html', 552 openid_form=AssociateOpenID, redirect_field_name=REDIRECT_FIELD_NAME, 553 on_failure=associate_failure, extra_context=None):
554 555 """View that allow a user to associate a new openid to its account. 556 557 :attr request: request object 558 :attr template_name: string, name of template to use, 559 'authopenid/associate.html' by default 560 :attr openid_form: form use enter openid url. By default 561 ``madrona.openid.forms.AssociateOpenID`` 562 :attr redirect_field_name: string, field name used for redirect. 563 by default 'next' 564 :attr on_success: callbale, function used when openid auth success 565 :attr on_failure: callable, function used when openid auth failed. 566 by default ``madrona.openid.views.associate_failure` 567 :attr extra_context: A dictionary of variables to add to the template 568 context. A callable object in this dictionary will be called to produce 569 the end result which appears in the context. 570 """ 571 572 redirect_to = request.REQUEST.get(redirect_field_name, '') 573 if request.POST: 574 form = openid_form(request.user, data=request.POST) 575 if form.is_valid(): 576 if not redirect_to or '//' in redirect_to or ' ' in redirect_to: 577 redirect_to = settings.LOGIN_REDIRECT_URL 578 redirect_url = "%s%s?%s" % ( 579 get_url_host(request), 580 reverse('user_complete_associate'), 581 urllib.urlencode({redirect_field_name: redirect_to}) 582 ) 583 return ask_openid(request, 584 form.cleaned_data['openid_url'], 585 redirect_url, 586 on_failure=on_failure) 587 else: 588 form = openid_form(request.user) 589 return render(template_name, { 590 'form': form, 591 redirect_field_name: redirect_to 592 }, context_instance=_build_context(request, extra_context=extra_context))
593
594 @login_required 595 -def dissociate(request, template_name="authopenid/dissociate.html", 596 dissociate_form=OpenidDissociateForm, 597 redirect_field_name=REDIRECT_FIELD_NAME, 598 default_redirect=settings.LOGIN_REDIRECT_URL, extra_context=None):
599 600 """ view used to dissociate an openid from an account """ 601 redirect_to = request.REQUEST.get(redirect_field_name, '') 602 if not redirect_to or '//' in redirect_to or ' ' in redirect_to: 603 redirect_to = default_redirect 604 605 # get list of associated openids 606 rels = UserAssociation.objects.filter(user__id=request.user.id) 607 associated_openids = [rel.openid_url for rel in rels] 608 if len(associated_openids) == 1 and not request.user.has_usable_password(): 609 msg = _("You can't remove this openid. " 610 "You should set a password first.") 611 return HttpResponseRedirect("%s?%s" % (redirect_to, 612 urllib.urlencode({"msg": msg}))) 613 614 if request.POST: 615 form = dissociate_form(request.POST) 616 if form.is_valid(): 617 openid_url = form.cleaned_data['openid_url'] 618 msg = "" 619 if openid_url not in associated_openids: 620 msg = _("%s is not associated to your account") % openid_url 621 622 if not msg: 623 UserAssociation.objects.get(openid_url__exact=openid_url).delete() 624 if openid_url == request.session.get('openid_url'): 625 del request.session['openid_url'] 626 msg = _("openid removed.") 627 return HttpResponseRedirect("%s?%s" % (redirect_to, 628 urllib.urlencode({"msg": msg}))) 629 else: 630 openid_url = request.GET.get('openid_url', '') 631 if not openid_url: 632 msg = _("Invalid OpenID url.") 633 return HttpResponseRedirect("%s?%s" % (redirect_to, 634 urllib.urlencode({"msg": msg}))) 635 form = dissociate_form(initial={'openid_url': openid_url}) 636 return render(template_name, { 637 "form": form, 638 "openid_url": openid_url 639 }, context_instance=_build_context(request, extra_context=extra_context))
640