Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# -*- coding: utf-8 -*- 

2# This program is distributed in the hope that it will be useful, but WITHOUT 

3# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 

4# FOR A PARTICULAR PURPOSE. See the GNU General Public License version 3 for 

5# more details. 

6# 

7# You should have received a copy of the GNU General Public License version 3 

8# along with this program; if not, write to the Free Software Foundation, Inc., 51 

9# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 

10# 

11# (c) 2015-2016 Valentin Samir 

12"""views for the app""" 

13from .default_settings import settings, SessionStore 

14 

15from django.shortcuts import render, redirect 

16from django.http import HttpResponse, HttpResponseRedirect 

17from django.contrib import messages 

18from django.utils.decorators import method_decorator 

19from django.utils.translation import ugettext as _ 

20from django.utils import timezone 

21from django.views.decorators.csrf import csrf_exempt 

22from django.middleware.csrf import CsrfViewMiddleware 

23from django.views.generic import View 

24from django.utils.encoding import python_2_unicode_compatible 

25from django.utils.safestring import mark_safe 

26try: 

27 from django.urls import reverse 

28except ImportError: 

29 from django.core.urlresolvers import reverse 

30 

31import re 

32import logging 

33import pprint 

34import requests 

35from lxml import etree 

36from datetime import timedelta 

37 

38import cas_server.utils as utils 

39import cas_server.forms as forms 

40import cas_server.models as models 

41 

42from .utils import json_response 

43from .models import Ticket, ServiceTicket, ProxyTicket, ProxyGrantingTicket 

44from .models import ServicePattern, FederatedIendityProvider, FederatedUser 

45from .federate import CASFederateValidateUser 

46 

47logger = logging.getLogger(__name__) 

48 

49 

50class LogoutMixin(object): 

51 """destroy CAS session utils""" 

52 

53 def logout(self, all_session=False): 

54 """ 

55 effectively destroy a CAS session 

56 

57 :param boolean all_session: If ``True`` destroy all the user sessions, otherwise 

58 destroy the current user session. 

59 :return: The number of destroyed sessions 

60 :rtype: int 

61 """ 

62 # initialize the counter of the number of destroyed sesisons 

63 session_nb = 0 

64 # save the current user username before flushing the session 

65 username = self.request.session.get("username") 

66 if username: 

67 if all_session: 

68 logger.info("Logging out user %s from all sessions." % username) 

69 else: 

70 logger.info("Logging out user %s." % username) 

71 users = [] 

72 # try to get the user from the current session 

73 try: 

74 users.append( 

75 models.User.objects.get( 

76 username=username, 

77 session_key=self.request.session.session_key 

78 ) 

79 ) 

80 except models.User.DoesNotExist: 

81 # if user not found in database, flush the session anyway 

82 self.request.session.flush() 

83 

84 # If all_session is set, search all of the user sessions 

85 if all_session: 

86 users.extend( 

87 models.User.objects.filter( 

88 username=username 

89 ).exclude( 

90 session_key=self.request.session.session_key 

91 ) 

92 ) 

93 

94 # Iterate over all user sessions that have to be logged out 

95 for user in users: 

96 # get the user session 

97 session = SessionStore(session_key=user.session_key) 

98 # flush the session 

99 session.flush() 

100 # send SLO requests 

101 user.logout(self.request) 

102 # delete the user 

103 user.delete() 

104 # increment the destroyed session counter 

105 session_nb += 1 

106 if username: 

107 logger.info("User %s logged out" % username) 

108 return session_nb 

109 

110 

111class CsrfExemptView(View): 

112 """base class for csrf exempt class views""" 

113 

114 @method_decorator(csrf_exempt) # csrf is disabled for allowing SLO requests reception 

115 def dispatch(self, request, *args, **kwargs): 

116 """ 

117 dispatch different http request to the methods of the same name 

118 

119 :param django.http.HttpRequest request: The current request object 

120 """ 

121 return super(CsrfExemptView, self).dispatch(request, *args, **kwargs) 

122 

123 

124class LogoutView(View, LogoutMixin): 

125 """destroy CAS session (logout) view""" 

126 

127 #: current :class:`django.http.HttpRequest` object 

128 request = None 

129 #: service GET parameter 

130 service = None 

131 #: url GET paramet 

132 url = None 

133 #: ``True`` if the HTTP_X_AJAX http header is sent and ``settings.CAS_ENABLE_AJAX_AUTH`` 

134 #: is ``True``, ``False`` otherwise. 

135 ajax = None 

136 

137 def init_get(self, request): 

138 """ 

139 Initialize the :class:`LogoutView` attributes on GET request 

140 

141 :param django.http.HttpRequest request: The current request object 

142 """ 

143 self.request = request 

144 self.service = request.GET.get('service') 

145 self.url = request.GET.get('url') 

146 self.ajax = settings.CAS_ENABLE_AJAX_AUTH and 'HTTP_X_AJAX' in request.META 

147 

148 def get(self, request, *args, **kwargs): 

149 """ 

150 method called on GET request on this view 

151 

152 :param django.http.HttpRequest request: The current request object 

153 """ 

154 logger.info("logout requested") 

155 # initialize the class attributes 

156 self.init_get(request) 

157 # if CAS federation mode is enable, bakup the provider before flushing the sessions 

158 if settings.CAS_FEDERATE: 

159 try: 

160 user = FederatedUser.get_from_federated_username( 

161 self.request.session.get("username") 

162 ) 

163 auth = CASFederateValidateUser(user.provider, service_url="") 

164 except FederatedUser.DoesNotExist: 

165 auth = None 

166 session_nb = self.logout(self.request.GET.get("all")) 

167 # if CAS federation mode is enable, redirect to user CAS logout page, appending the 

168 # current querystring 

169 if settings.CAS_FEDERATE: 

170 if auth is not None: 

171 params = utils.copy_params(request.GET, ignore={"forget_provider"}) 

172 url = auth.get_logout_url() 

173 response = HttpResponseRedirect(utils.update_url(url, params)) 

174 if request.GET.get("forget_provider"): 

175 response.delete_cookie("remember_provider") 

176 return response 

177 # if service is set, redirect to service after logout 

178 if self.service: 

179 list(messages.get_messages(request)) # clean messages before leaving the django app 

180 return HttpResponseRedirect(self.service) 

181 # if service is not set but url is set, redirect to url after logout 

182 elif self.url: 

183 list(messages.get_messages(request)) # clean messages before leaving the django app 

184 return HttpResponseRedirect(self.url) 

185 else: 

186 # build logout message depending of the number of sessions the user logs out 

187 if session_nb == 1: 

188 logout_msg = mark_safe(_( 

189 "<h3>Logout successful</h3>" 

190 "You have successfully logged out from the Central Authentication Service. " 

191 "For security reasons, close your web browser." 

192 )) 

193 elif session_nb > 1: 

194 logout_msg = mark_safe(_( 

195 "<h3>Logout successful</h3>" 

196 "You have successfully logged out from %d sessions of the Central " 

197 "Authentication Service. " 

198 "For security reasons, close your web browser." 

199 ) % session_nb) 

200 else: 

201 logout_msg = mark_safe(_( 

202 "<h3>Logout successful</h3>" 

203 "You were already logged out from the Central Authentication Service. " 

204 "For security reasons, close your web browser." 

205 )) 

206 

207 # depending of settings, redirect to the login page with a logout message or display 

208 # the logout page. The default is to display tge logout page. 

209 if settings.CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT: 

210 messages.add_message(request, messages.SUCCESS, logout_msg) 

211 if self.ajax: 

212 url = reverse("cas_server:login") 

213 data = { 

214 'status': 'success', 

215 'detail': 'logout', 

216 'url': url, 

217 'session_nb': session_nb 

218 } 

219 return json_response(request, data) 

220 else: 

221 return redirect("cas_server:login") 

222 else: 

223 if self.ajax: 

224 data = {'status': 'success', 'detail': 'logout', 'session_nb': session_nb} 

225 return json_response(request, data) 

226 else: 

227 return render( 

228 request, 

229 settings.CAS_LOGOUT_TEMPLATE, 

230 utils.context({'logout_msg': logout_msg}) 

231 ) 

232 

233 

234class FederateAuth(CsrfExemptView): 

235 """ 

236 view to authenticated user against a backend CAS then CAS_FEDERATE is True 

237 

238 csrf is disabled for allowing SLO requests reception. 

239 """ 

240 

241 #: current URL used as service URL by the CAS client 

242 service_url = None 

243 

244 def get_cas_client(self, request, provider, renew=False): 

245 """ 

246 return a CAS client object matching provider 

247 

248 :param django.http.HttpRequest request: The current request object 

249 :param cas_server.models.FederatedIendityProvider provider: the user identity provider 

250 :return: The user CAS client object 

251 :rtype: :class:`federate.CASFederateValidateUser 

252 <cas_server.federate.CASFederateValidateUser>` 

253 """ 

254 # compute the current url, ignoring ticket dans provider GET parameters 

255 service_url = utils.get_current_url(request, {"ticket", "provider"}) 

256 self.service_url = service_url 

257 return CASFederateValidateUser(provider, service_url, renew=renew) 

258 

259 def post(self, request, provider=None): 

260 """ 

261 method called on POST request 

262 

263 :param django.http.HttpRequest request: The current request object 

264 :param unicode provider: Optional parameter. The user provider suffix. 

265 """ 

266 # if settings.CAS_FEDERATE is not True redirect to the login page 

267 if not settings.CAS_FEDERATE: 

268 logger.warning("CAS_FEDERATE is False, set it to True to use federation") 

269 return redirect("cas_server:login") 

270 # POST with a provider suffix, this is probably an SLO request. csrf is disabled for 

271 # allowing SLO requests reception 

272 try: 

273 provider = FederatedIendityProvider.objects.get(suffix=provider) 

274 auth = self.get_cas_client(request, provider) 

275 try: 

276 auth.clean_sessions(request.POST['logoutRequest']) 

277 except (KeyError, AttributeError): 

278 pass 

279 return HttpResponse("ok") 

280 # else, a User is trying to log in using an identity provider 

281 except FederatedIendityProvider.DoesNotExist: 

282 # Manually checking for csrf to protect the code below 

283 reason = CsrfViewMiddleware().process_view(request, None, (), {}) 

284 if reason is not None: # pragma: no cover (csrf checks are disabled during tests) 

285 return reason # Failed the test, stop here. 

286 form = forms.FederateSelect(request.POST) 

287 if form.is_valid(): 

288 params = utils.copy_params( 

289 request.POST, 

290 ignore={"provider", "csrfmiddlewaretoken", "ticket", "lt"} 

291 ) 

292 if params.get("renew") == "False": 

293 del params["renew"] 

294 url = utils.reverse_params( 

295 "cas_server:federateAuth", 

296 kwargs=dict(provider=form.cleaned_data["provider"].suffix), 

297 params=params 

298 ) 

299 return HttpResponseRedirect(url) 

300 else: 

301 return redirect("cas_server:login") 

302 

303 def get(self, request, provider=None): 

304 """ 

305 method called on GET request 

306 

307 :param django.http.HttpRequestself. request: The current request object 

308 :param unicode provider: Optional parameter. The user provider suffix. 

309 """ 

310 # if settings.CAS_FEDERATE is not True redirect to the login page 

311 if not settings.CAS_FEDERATE: 

312 logger.warning("CAS_FEDERATE is False, set it to True to use federation") 

313 return redirect("cas_server:login") 

314 renew = bool(request.GET.get('renew') and request.GET['renew'] != "False") 

315 # Is the user is already authenticated, no need to request authentication to the user 

316 # identity provider. 

317 if self.request.session.get("authenticated") and not renew: 

318 logger.warning("User already authenticated, dropping federated authentication request") 

319 return redirect("cas_server:login") 

320 try: 

321 # get the identity provider from its suffix 

322 provider = FederatedIendityProvider.objects.get(suffix=provider) 

323 # get a CAS client for the user identity provider 

324 auth = self.get_cas_client(request, provider, renew) 

325 # if no ticket submited, redirect to the identity provider CAS login page 

326 if 'ticket' not in request.GET: 

327 logger.info("Trying to authenticate %s again" % auth.provider.server_url) 

328 return HttpResponseRedirect(auth.get_login_url()) 

329 else: 

330 ticket = request.GET['ticket'] 

331 try: 

332 # if the ticket validation succeed 

333 if auth.verify_ticket(ticket): 

334 logger.info( 

335 "Got a valid ticket for %s from %s" % ( 

336 auth.username, 

337 auth.provider.server_url 

338 ) 

339 ) 

340 params = utils.copy_params(request.GET, ignore={"ticket", "remember"}) 

341 request.session["federate_username"] = auth.federated_username 

342 request.session["federate_ticket"] = ticket 

343 auth.register_slo( 

344 auth.federated_username, 

345 request.session.session_key, 

346 ticket 

347 ) 

348 # redirect to the the login page for the user to become authenticated 

349 # thanks to the `federate_username` and `federate_ticket` session parameters 

350 url = utils.reverse_params("cas_server:login", params) 

351 response = HttpResponseRedirect(url) 

352 # If the user has checked "remember my identity provider" store it in a 

353 # cookie 

354 if request.GET.get("remember"): 

355 max_age = settings.CAS_FEDERATE_REMEMBER_TIMEOUT 

356 utils.set_cookie( 

357 response, 

358 "remember_provider", 

359 provider.suffix, 

360 max_age 

361 ) 

362 return response 

363 # else redirect to the identity provider CAS login page 

364 else: 

365 logger.info( 

366 ( 

367 "Got an invalid ticket %s from %s for service %s. " 

368 "Retrying authentication" 

369 ) % ( 

370 ticket, 

371 auth.provider.server_url, 

372 self.service_url 

373 ) 

374 ) 

375 return HttpResponseRedirect(auth.get_login_url()) 

376 # both xml.etree.ElementTree and lxml.etree exceptions inherit from SyntaxError 

377 except SyntaxError as error: 

378 messages.add_message( 

379 request, 

380 messages.ERROR, 

381 _( 

382 u"Invalid response from your identity provider CAS upon " 

383 u"ticket %(ticket)s validation: %(error)r" 

384 ) % {'ticket': ticket, 'error': error} 

385 ) 

386 response = redirect("cas_server:login") 

387 response.delete_cookie("remember_provider") 

388 return response 

389 except FederatedIendityProvider.DoesNotExist: 

390 logger.warning("Identity provider suffix %s not found" % provider) 

391 # if the identity provider is not found, redirect to the login page 

392 return redirect("cas_server:login") 

393 

394 

395class LoginView(View, LogoutMixin): 

396 """credential requestor / acceptor""" 

397 

398 # pylint: disable=too-many-instance-attributes 

399 # Nine is reasonable in this case. 

400 

401 #: The current :class:`models.User<cas_server.models.User>` object 

402 user = None 

403 #: The form to display to the user 

404 form = None 

405 

406 #: current :class:`django.http.HttpRequest` object 

407 request = None 

408 #: service GET/POST parameter 

409 service = None 

410 #: ``True`` if renew GET/POST parameter is present and not "False" 

411 renew = None 

412 #: the warn GET/POST parameter 

413 warn = None 

414 #: the gateway GET/POST parameter 

415 gateway = None 

416 #: the method GET/POST parameter 

417 method = None 

418 

419 #: ``True`` if the HTTP_X_AJAX http header is sent and ``settings.CAS_ENABLE_AJAX_AUTH`` 

420 #: is ``True``, ``False`` otherwise. 

421 ajax = None 

422 

423 #: ``True`` if the user has just authenticated 

424 renewed = False 

425 #: ``True`` if renew GET/POST parameter is present and not "False" 

426 warned = False 

427 

428 #: The :class:`FederateAuth` transmited username (only used if ``settings.CAS_FEDERATE`` 

429 #: is ``True``) 

430 username = None 

431 #: The :class:`FederateAuth` transmited ticket (only used if ``settings.CAS_FEDERATE`` is 

432 #: ``True``) 

433 ticket = None 

434 

435 INVALID_LOGIN_TICKET = 1 

436 USER_LOGIN_OK = 2 

437 USER_LOGIN_FAILURE = 3 

438 USER_ALREADY_LOGGED = 4 

439 USER_AUTHENTICATED = 5 

440 USER_NOT_AUTHENTICATED = 6 

441 

442 def init_post(self, request): 

443 """ 

444 Initialize POST received parameters 

445 

446 :param django.http.HttpRequest request: The current request object 

447 """ 

448 self.request = request 

449 self.service = request.POST.get('service') 

450 self.renew = bool(request.POST.get('renew') and request.POST['renew'] != "False") 

451 self.gateway = request.POST.get('gateway') 

452 self.method = request.POST.get('method') 

453 self.ajax = settings.CAS_ENABLE_AJAX_AUTH and 'HTTP_X_AJAX' in request.META 

454 if request.POST.get('warned') and request.POST['warned'] != "False": 

455 self.warned = True 

456 self.warn = request.POST.get('warn') 

457 if settings.CAS_FEDERATE: 

458 self.username = request.POST.get('username') 

459 # in federated mode, the valdated indentity provider CAS ticket is used as password 

460 self.ticket = request.POST.get('password') 

461 

462 def gen_lt(self): 

463 """Generate a new LoginTicket and add it to the list of valid LT for the user""" 

464 self.request.session['lt'] = self.request.session.get('lt', []) + [utils.gen_lt()] 

465 if len(self.request.session['lt']) > 100: 

466 self.request.session['lt'] = self.request.session['lt'][-100:] 

467 

468 def check_lt(self): 

469 """ 

470 Check is the POSTed LoginTicket is valid, if yes invalide it 

471 

472 :return: ``True`` if the LoginTicket is valid, ``False`` otherwise 

473 :rtype: bool 

474 """ 

475 # save LT for later check 

476 lt_valid = self.request.session.get('lt', []) 

477 lt_send = self.request.POST.get('lt') 

478 # generate a new LT (by posting the LT has been consumed) 

479 self.gen_lt() 

480 # check if send LT is valid 

481 if lt_send not in lt_valid: 

482 return False 

483 else: 

484 self.request.session['lt'].remove(lt_send) 

485 # we need to redo the affectation for django to detect that the list has changed 

486 # and for its new value to be store in the session 

487 self.request.session['lt'] = self.request.session['lt'] 

488 return True 

489 

490 def post(self, request, *args, **kwargs): 

491 """ 

492 method called on POST request on this view 

493 

494 :param django.http.HttpRequest request: The current request object 

495 """ 

496 # initialize class parameters 

497 self.init_post(request) 

498 # process the POST request 

499 ret = self.process_post() 

500 if ret == self.INVALID_LOGIN_TICKET: 

501 messages.add_message( 

502 self.request, 

503 messages.ERROR, 

504 _(u"Invalid login ticket, please try to log in again") 

505 ) 

506 elif ret == self.USER_LOGIN_OK: 

507 # On successful login, update the :class:`models.User<cas_server.models.User>` ``date`` 

508 # attribute by saving it. (``auto_now=True``) 

509 self.user = models.User.objects.get_or_create( 

510 username=self.request.session['username'], 

511 session_key=self.request.session.session_key 

512 )[0] 

513 self.user.last_login = timezone.now() 

514 self.user.save() 

515 elif ret == self.USER_LOGIN_FAILURE: # bad user login 

516 if settings.CAS_FEDERATE: 

517 self.ticket = None 

518 self.username = None 

519 self.init_form() 

520 # preserve valid LoginTickets from session flush 

521 lt = self.request.session.get('lt', []) 

522 # On login failure, flush the session 

523 self.logout() 

524 # restore valid LoginTickets 

525 self.request.session['lt'] = lt 

526 elif ret == self.USER_ALREADY_LOGGED: 

527 pass 

528 else: # pragma: no cover (should no happen) 

529 raise EnvironmentError("invalid output for LoginView.process_post") 

530 # call the GET/POST common part 

531 response = self.common() 

532 if self.warn: 

533 utils.set_cookie( 

534 response, 

535 "warn", 

536 "on", 

537 10 * 365 * 24 * 3600 

538 ) 

539 else: 

540 response.delete_cookie("warn") 

541 return response 

542 

543 def process_post(self): 

544 """ 

545 Analyse the POST request: 

546 

547 * check that the LoginTicket is valid 

548 * check that the user sumited credentials are valid 

549 

550 :return: 

551 * :attr:`INVALID_LOGIN_TICKET` if the POSTed LoginTicket is not valid 

552 * :attr:`USER_ALREADY_LOGGED` if the user is already logged and do no request 

553 reauthentication. 

554 * :attr:`USER_LOGIN_FAILURE` if the user is not logged or request for 

555 reauthentication and his credentials are not valid 

556 * :attr:`USER_LOGIN_OK` if the user is not logged or request for 

557 reauthentication and his credentials are valid 

558 :rtype: int 

559 """ 

560 if not self.check_lt(): 

561 self.init_form(self.request.POST) 

562 logger.warning("Received an invalid login ticket") 

563 return self.INVALID_LOGIN_TICKET 

564 elif not self.request.session.get("authenticated") or self.renew: 

565 # authentication request receive, initialize the form to use 

566 self.init_form(self.request.POST) 

567 if self.form.is_valid(): 

568 self.request.session.set_expiry(0) 

569 self.request.session["username"] = self.form.cleaned_data['username'] 

570 self.request.session["warn"] = True if self.form.cleaned_data.get("warn") else False 

571 self.request.session["authenticated"] = True 

572 self.renewed = True 

573 self.warned = True 

574 logger.info("User %s successfully authenticated" % self.request.session["username"]) 

575 return self.USER_LOGIN_OK 

576 else: 

577 logger.warning("A login attempt failed") 

578 return self.USER_LOGIN_FAILURE 

579 else: 

580 logger.warning("Received a login attempt for an already-active user") 

581 return self.USER_ALREADY_LOGGED 

582 

583 def init_get(self, request): 

584 """ 

585 Initialize GET received parameters 

586 

587 :param django.http.HttpRequest request: The current request object 

588 """ 

589 self.request = request 

590 self.service = request.GET.get('service') 

591 self.renew = bool(request.GET.get('renew') and request.GET['renew'] != "False") 

592 self.gateway = request.GET.get('gateway') 

593 self.method = request.GET.get('method') 

594 self.ajax = settings.CAS_ENABLE_AJAX_AUTH and 'HTTP_X_AJAX' in request.META 

595 self.warn = request.GET.get('warn') 

596 if settings.CAS_FEDERATE: 

597 # here username and ticket are fetch from the session after a redirection from 

598 # FederateAuth.get 

599 self.username = request.session.get("federate_username") 

600 self.ticket = request.session.get("federate_ticket") 

601 if self.username: 

602 del request.session["federate_username"] 

603 if self.ticket: 

604 del request.session["federate_ticket"] 

605 

606 def get(self, request, *args, **kwargs): 

607 """ 

608 method called on GET request on this view 

609 

610 :param django.http.HttpRequest request: The current request object 

611 """ 

612 # initialize class parameters 

613 self.init_get(request) 

614 # process the GET request 

615 self.process_get() 

616 # call the GET/POST common part 

617 return self.common() 

618 

619 def process_get(self): 

620 """ 

621 Analyse the GET request 

622 

623 :return: 

624 * :attr:`USER_NOT_AUTHENTICATED` if the user is not authenticated or is requesting 

625 for authentication renewal 

626 * :attr:`USER_AUTHENTICATED` if the user is authenticated and is not requesting 

627 for authentication renewal 

628 :rtype: int 

629 """ 

630 # generate a new LT 

631 self.gen_lt() 

632 if not self.request.session.get("authenticated") or self.renew: 

633 # authentication will be needed, initialize the form to use 

634 self.init_form() 

635 return self.USER_NOT_AUTHENTICATED 

636 return self.USER_AUTHENTICATED 

637 

638 def init_form(self, values=None): 

639 """ 

640 Initialization of the good form depending of POST and GET parameters 

641 

642 :param django.http.QueryDict values: A POST or GET QueryDict 

643 """ 

644 if values: 

645 values = values.copy() 

646 values['lt'] = self.request.session['lt'][-1] 

647 form_initial = { 

648 'service': self.service, 

649 'method': self.method, 

650 'warn': ( 

651 self.warn or self.request.session.get("warn") or self.request.COOKIES.get('warn') 

652 ), 

653 'lt': self.request.session['lt'][-1], 

654 'renew': self.renew 

655 } 

656 if settings.CAS_FEDERATE: 

657 if self.username and self.ticket: 

658 form_initial['username'] = self.username 

659 form_initial['password'] = self.ticket 

660 form_initial['ticket'] = self.ticket 

661 self.form = forms.FederateUserCredential( 

662 values, 

663 initial=form_initial 

664 ) 

665 else: 

666 self.form = forms.FederateSelect(values, initial=form_initial) 

667 else: 

668 self.form = forms.UserCredential( 

669 values, 

670 initial=form_initial 

671 ) 

672 

673 def service_login(self): 

674 """ 

675 Perform login against a service 

676 

677 :return: 

678 * The rendering of the ``settings.CAS_WARN_TEMPLATE`` if the user asked to be 

679 warned before ticket emission and has not yep been warned. 

680 * The redirection to the service URL with a ticket GET parameter 

681 * The redirection to the service URL without a ticket if ticket generation failed 

682 and the :attr:`gateway` attribute is set 

683 * The rendering of the ``settings.CAS_LOGGED_TEMPLATE`` template with some error 

684 messages if the ticket generation failed (e.g: user not allowed). 

685 :rtype: django.http.HttpResponse 

686 """ 

687 try: 

688 # is the service allowed 

689 service_pattern = ServicePattern.validate(self.service) 

690 # is the current user allowed on this service 

691 service_pattern.check_user(self.user) 

692 # if the user has asked to be warned before any login to a service 

693 if self.request.session.get("warn", True) and not self.warned: 

694 messages.add_message( 

695 self.request, 

696 messages.WARNING, 

697 _(u"Authentication has been required by service %(name)s (%(url)s)") % 

698 {'name': service_pattern.name, 'url': self.service} 

699 ) 

700 if self.ajax: 

701 data = {"status": "error", "detail": "confirmation needed"} 

702 return json_response(self.request, data) 

703 else: 

704 warn_form = forms.WarnForm(initial={ 

705 'service': self.service, 

706 'renew': self.renew, 

707 'gateway': self.gateway, 

708 'method': self.method, 

709 'warned': True, 

710 'lt': self.request.session['lt'][-1] 

711 }) 

712 return render( 

713 self.request, 

714 settings.CAS_WARN_TEMPLATE, 

715 utils.context({'form': warn_form}) 

716 ) 

717 else: 

718 # redirect, using method ? 

719 list(messages.get_messages(self.request)) # clean messages before leaving django 

720 redirect_url = self.user.get_service_url( 

721 self.service, 

722 service_pattern, 

723 renew=self.renewed 

724 ) 

725 if not self.ajax: 

726 return HttpResponseRedirect(redirect_url) 

727 else: 

728 data = {"status": "success", "detail": "auth", "url": redirect_url} 

729 return json_response(self.request, data) 

730 except ServicePattern.DoesNotExist: 

731 error = 1 

732 messages.add_message( 

733 self.request, 

734 messages.ERROR, 

735 _(u'Service %(url)s not allowed.') % {'url': self.service} 

736 ) 

737 except models.BadUsername: 

738 error = 2 

739 messages.add_message( 

740 self.request, 

741 messages.ERROR, 

742 _(u"Username not allowed") 

743 ) 

744 except models.BadFilter: 

745 error = 3 

746 messages.add_message( 

747 self.request, 

748 messages.ERROR, 

749 _(u"User characteristics not allowed") 

750 ) 

751 except models.UserFieldNotDefined: 

752 error = 4 

753 messages.add_message( 

754 self.request, 

755 messages.ERROR, 

756 _(u"The attribute %(field)s is needed to use" 

757 u" that service") % {'field': service_pattern.user_field} 

758 ) 

759 

760 # if gateway is set and auth failed redirect to the service without authentication 

761 if self.gateway and not self.ajax: 

762 list(messages.get_messages(self.request)) # clean messages before leaving django 

763 return HttpResponseRedirect(self.service) 

764 

765 if not self.ajax: 

766 return render( 

767 self.request, 

768 settings.CAS_LOGGED_TEMPLATE, 

769 utils.context({'session': self.request.session}) 

770 ) 

771 else: 

772 data = {"status": "error", "detail": "auth", "code": error} 

773 return json_response(self.request, data) 

774 

775 def authenticated(self): 

776 """ 

777 Processing authenticated users 

778 

779 :return: 

780 * The returned value of :meth:`service_login` if :attr:`service` is defined 

781 * The rendering of ``settings.CAS_LOGGED_TEMPLATE`` otherwise 

782 :rtype: django.http.HttpResponse 

783 """ 

784 # Try to get the current :class:`models.User<cas_server.models.User>` object for the current 

785 # session 

786 try: 

787 self.user = models.User.objects.get( 

788 username=self.request.session.get("username"), 

789 session_key=self.request.session.session_key 

790 ) 

791 # if not found, flush the session and redirect to the login page 

792 except models.User.DoesNotExist: 

793 logger.warning( 

794 "User %s seems authenticated but is not found in the database." % ( 

795 self.request.session.get("username"), 

796 ) 

797 ) 

798 self.logout() 

799 if self.ajax: 

800 data = { 

801 "status": "error", 

802 "detail": "login required", 

803 "url": utils.reverse_params("cas_server:login", params=self.request.GET) 

804 } 

805 return json_response(self.request, data) 

806 else: 

807 return utils.redirect_params("cas_server:login", params=self.request.GET) 

808 

809 # if login against a service 

810 if self.service: 

811 return self.service_login() 

812 # else display the logged template 

813 else: 

814 if self.ajax: 

815 data = {"status": "success", "detail": "logged"} 

816 return json_response(self.request, data) 

817 else: 

818 return render( 

819 self.request, 

820 settings.CAS_LOGGED_TEMPLATE, 

821 utils.context({'session': self.request.session}) 

822 ) 

823 

824 def not_authenticated(self): 

825 """ 

826 Processing non authenticated users 

827 

828 :return: 

829 * The rendering of ``settings.CAS_LOGIN_TEMPLATE`` with various messages 

830 depending of GET/POST parameters 

831 * The redirection to :class:`FederateAuth` if ``settings.CAS_FEDERATE`` is ``True`` 

832 and the "remember my identity provider" cookie is found 

833 :rtype: django.http.HttpResponse 

834 """ 

835 if self.service: 

836 try: 

837 service_pattern = ServicePattern.validate(self.service) 

838 if self.gateway and not self.ajax: 

839 # clean messages before leaving django 

840 list(messages.get_messages(self.request)) 

841 return HttpResponseRedirect(self.service) 

842 

843 if settings.CAS_SHOW_SERVICE_MESSAGES: 

844 if self.request.session.get("authenticated") and self.renew: 

845 messages.add_message( 

846 self.request, 

847 messages.WARNING, 

848 _(u"Authentication renewal required by service %(name)s (%(url)s).") % 

849 {'name': service_pattern.name, 'url': self.service} 

850 ) 

851 else: 

852 messages.add_message( 

853 self.request, 

854 messages.WARNING, 

855 _(u"Authentication required by service %(name)s (%(url)s).") % 

856 {'name': service_pattern.name, 'url': self.service} 

857 ) 

858 except ServicePattern.DoesNotExist: 

859 if settings.CAS_SHOW_SERVICE_MESSAGES: 

860 messages.add_message( 

861 self.request, 

862 messages.ERROR, 

863 _(u'Service %s not allowed') % self.service 

864 ) 

865 if self.ajax: 

866 data = { 

867 "status": "error", 

868 "detail": "login required", 

869 "url": utils.reverse_params("cas_server:login", params=self.request.GET) 

870 } 

871 return json_response(self.request, data) 

872 else: 

873 if settings.CAS_FEDERATE: 

874 if self.username and self.ticket: 

875 return render( 

876 self.request, 

877 settings.CAS_LOGIN_TEMPLATE, 

878 utils.context({ 

879 'form': self.form, 

880 'auto_submit': True, 

881 'post_url': reverse("cas_server:login") 

882 }) 

883 ) 

884 else: 

885 if ( 

886 self.request.COOKIES.get('remember_provider') and 

887 FederatedIendityProvider.objects.filter( 

888 suffix=self.request.COOKIES['remember_provider'] 

889 ) 

890 ): 

891 params = utils.copy_params(self.request.GET) 

892 url = utils.reverse_params( 

893 "cas_server:federateAuth", 

894 params=params, 

895 kwargs=dict(provider=self.request.COOKIES['remember_provider']) 

896 ) 

897 return HttpResponseRedirect(url) 

898 else: 

899 # if user is authenticated and auth renewal is requested, redirect directly 

900 # to the user identity provider 

901 if self.renew and self.request.session.get("authenticated"): 

902 try: 

903 user = FederatedUser.get_from_federated_username( 

904 self.request.session.get("username") 

905 ) 

906 params = utils.copy_params(self.request.GET) 

907 url = utils.reverse_params( 

908 "cas_server:federateAuth", 

909 params=params, 

910 kwargs=dict(provider=user.provider.suffix) 

911 ) 

912 return HttpResponseRedirect(url) 

913 # Should normally not happen: if the user is logged, it exists in the 

914 # database. 

915 except FederatedUser.DoesNotExist: # pragma: no cover 

916 pass 

917 return render( 

918 self.request, 

919 settings.CAS_LOGIN_TEMPLATE, 

920 utils.context({ 

921 'form': self.form, 

922 'post_url': reverse("cas_server:federateAuth") 

923 }) 

924 ) 

925 else: 

926 return render( 

927 self.request, 

928 settings.CAS_LOGIN_TEMPLATE, 

929 utils.context({'form': self.form}) 

930 ) 

931 

932 def common(self): 

933 """ 

934 Common part execute uppon GET and POST request 

935 

936 :return: 

937 * The returned value of :meth:`authenticated` if the user is authenticated and 

938 not requesting for authentication or if the authentication has just been renewed 

939 * The returned value of :meth:`not_authenticated` otherwise 

940 :rtype: django.http.HttpResponse 

941 """ 

942 # if authenticated and successfully renewed authentication if needed 

943 if self.request.session.get("authenticated") and (not self.renew or self.renewed): 

944 return self.authenticated() 

945 else: 

946 return self.not_authenticated() 

947 

948 

949class Auth(CsrfExemptView): 

950 """ 

951 A simple view to validate username/password/service tuple 

952 

953 csrf is disable as it is intended to be used by programs. Security is assured by a shared 

954 secret between the programs dans django-cas-server. 

955 """ 

956 

957 @staticmethod 

958 def post(request): 

959 """ 

960 method called on POST request on this view 

961 

962 :param django.http.HttpRequest request: The current request object 

963 :return: ``HttpResponse(u"yes\\n")`` if the POSTed tuple (username, password, service) 

964 if valid (i.e. (username, password) is valid dans username is allowed on service). 

965 ``HttpResponse(u"no\\n…")`` otherwise, with possibly an error message on the second 

966 line. 

967 :rtype: django.http.HttpResponse 

968 """ 

969 username = request.POST.get('username') 

970 password = request.POST.get('password') 

971 service = request.POST.get('service') 

972 secret = request.POST.get('secret') 

973 

974 if not settings.CAS_AUTH_SHARED_SECRET: 

975 return HttpResponse( 

976 "no\nplease set CAS_AUTH_SHARED_SECRET", 

977 content_type="text/plain; charset=utf-8" 

978 ) 

979 if secret != settings.CAS_AUTH_SHARED_SECRET: 

980 return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8") 

981 if not username or not password or not service: 

982 return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8") 

983 form = forms.UserCredential( 

984 request.POST, 

985 initial={ 

986 'service': service, 

987 'method': 'POST', 

988 'warn': False 

989 } 

990 ) 

991 if form.is_valid(): 

992 try: 

993 user = models.User.objects.get_or_create( 

994 username=form.cleaned_data['username'], 

995 session_key=request.session.session_key 

996 )[0] 

997 user.save() 

998 # is the service allowed 

999 service_pattern = ServicePattern.validate(service) 

1000 # is the current user allowed on this service 

1001 service_pattern.check_user(user) 

1002 if not request.session.get("authenticated"): 

1003 user.delete() 

1004 return HttpResponse(u"yes\n", content_type="text/plain; charset=utf-8") 

1005 except (ServicePattern.DoesNotExist, models.ServicePatternException): 

1006 return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8") 

1007 else: 

1008 return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8") 

1009 

1010 

1011class Validate(View): 

1012 """service ticket validation""" 

1013 @staticmethod 

1014 def get(request): 

1015 """ 

1016 method called on GET request on this view 

1017 

1018 :param django.http.HttpRequest request: The current request object 

1019 :return: 

1020 * ``HttpResponse("yes\\nusername")`` if submited (service, ticket) is valid 

1021 * else ``HttpResponse("no\\n")`` 

1022 :rtype: django.http.HttpResponse 

1023 """ 

1024 # store wanted GET parameters 

1025 service = request.GET.get('service') 

1026 ticket = request.GET.get('ticket') 

1027 renew = True if request.GET.get('renew') else False 

1028 # service and ticket parameters are mandatory 

1029 if service and ticket: 

1030 try: 

1031 # search for the ticket, associated at service that is not yet validated but is 

1032 # still valid 

1033 ticket = ServiceTicket.get(ticket, renew, service) 

1034 logger.info( 

1035 "Validate: Service ticket %s validated, user %s authenticated on service %s" % ( 

1036 ticket.value, 

1037 ticket.user.username, 

1038 ticket.service 

1039 ) 

1040 ) 

1041 return HttpResponse( 

1042 u"yes\n%s\n" % ticket.username(), 

1043 content_type="text/plain; charset=utf-8" 

1044 ) 

1045 except ServiceTicket.DoesNotExist: 

1046 logger.warning( 

1047 ( 

1048 "Validate: Service ticket %s not found or " 

1049 "already validated, auth to %s failed" 

1050 ) % ( 

1051 ticket, 

1052 service 

1053 ) 

1054 ) 

1055 return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8") 

1056 else: 

1057 logger.warning("Validate: service or ticket missing") 

1058 return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8") 

1059 

1060 

1061@python_2_unicode_compatible 

1062class ValidationBaseError(Exception): 

1063 """Base class for both saml and cas validation error""" 

1064 

1065 #: The error code 

1066 code = None 

1067 #: The error message 

1068 msg = None 

1069 

1070 def __init__(self, code, msg=""): 

1071 self.code = code 

1072 self.msg = msg 

1073 super(ValidationBaseError, self).__init__(code) 

1074 

1075 def __str__(self): 

1076 return u"%s" % self.msg 

1077 

1078 def render(self, request): 

1079 """ 

1080 render the error template for the exception 

1081 

1082 :param django.http.HttpRequest request: The current request object: 

1083 :return: the rendered ``cas_server/serviceValidateError.xml`` template 

1084 :rtype: django.http.HttpResponse 

1085 """ 

1086 return render( 

1087 request, 

1088 self.template, 

1089 self.context(), content_type="text/xml; charset=utf-8" 

1090 ) 

1091 

1092 

1093class ValidateError(ValidationBaseError): 

1094 """handle service validation error""" 

1095 

1096 #: template to be render for the error 

1097 template = "cas_server/serviceValidateError.xml" 

1098 

1099 def context(self): 

1100 """ 

1101 content to use to render :attr:`template` 

1102 

1103 :return: A dictionary to contextualize :attr:`template` 

1104 :rtype: dict 

1105 """ 

1106 return {'code': self.code, 'msg': self.msg} 

1107 

1108 

1109class ValidateService(View): 

1110 """service ticket validation [CAS 2.0] and [CAS 3.0]""" 

1111 #: Current :class:`django.http.HttpRequest` object 

1112 request = None 

1113 #: The service GET parameter 

1114 service = None 

1115 #: the ticket GET parameter 

1116 ticket = None 

1117 #: the pgtUrl GET parameter 

1118 pgt_url = None 

1119 #: the renew GET parameter 

1120 renew = None 

1121 #: specify if ProxyTicket are allowed by the view. Hence we user the same view for 

1122 #: ``/serviceValidate`` and ``/proxyValidate`` juste changing the parameter. 

1123 allow_proxy_ticket = False 

1124 

1125 def get(self, request): 

1126 """ 

1127 method called on GET request on this view 

1128 

1129 :param django.http.HttpRequest request: The current request object: 

1130 :return: The rendering of ``cas_server/serviceValidate.xml`` if no errors is raised, 

1131 the rendering or ``cas_server/serviceValidateError.xml`` otherwise. 

1132 :rtype: django.http.HttpResponse 

1133 """ 

1134 # define the class parameters 

1135 self.request = request 

1136 self.service = request.GET.get('service') 

1137 self.ticket = request.GET.get('ticket') 

1138 self.pgt_url = request.GET.get('pgtUrl') 

1139 self.renew = True if request.GET.get('renew') else False 

1140 

1141 # service and ticket parameter are mandatory 

1142 if not self.service or not self.ticket: 

1143 logger.warning("ValidateService: missing ticket or service") 

1144 return ValidateError( 

1145 u'INVALID_REQUEST', 

1146 u"you must specify a service and a ticket" 

1147 ).render(request) 

1148 else: 

1149 try: 

1150 # search the ticket in the database 

1151 self.ticket, proxies = self.process_ticket() 

1152 # prepare template rendering context 

1153 params = { 

1154 'username': self.ticket.username(), 

1155 'attributes': self.ticket.attributs_flat(), 

1156 'proxies': proxies, 

1157 'auth_date': self.ticket.user.last_login.replace(microsecond=0).isoformat(), 

1158 'is_new_login': 'true' if self.ticket.renew else 'false' 

1159 } 

1160 # if pgtUrl is set, require https or localhost 

1161 if self.pgt_url and ( 

1162 self.pgt_url.startswith("https://") or 

1163 re.match(r"^http://(127\.0\.0\.1|localhost)(:[0-9]+)?(/.*)?$", self.pgt_url) 

1164 ): 

1165 return self.process_pgturl(params) 

1166 else: 

1167 logger.info( 

1168 "ValidateService: ticket %s validated for user %s on service %s." % ( 

1169 self.ticket.value, 

1170 self.ticket.user.username, 

1171 self.ticket.service 

1172 ) 

1173 ) 

1174 logger.debug( 

1175 "ValidateService: User attributs are:\n%s" % ( 

1176 pprint.pformat(self.ticket.attributs), 

1177 ) 

1178 ) 

1179 return render( 

1180 request, 

1181 "cas_server/serviceValidate.xml", 

1182 params, 

1183 content_type="text/xml; charset=utf-8" 

1184 ) 

1185 except ValidateError as error: 

1186 logger.warning( 

1187 "ValidateService: validation error: %s %s" % (error.code, error.msg) 

1188 ) 

1189 return error.render(request) 

1190 

1191 def process_ticket(self): 

1192 """ 

1193 fetch the ticket against the database and check its validity 

1194 

1195 :raises ValidateError: if the ticket is not found or not valid, potentially for that 

1196 service 

1197 :returns: A couple (ticket, proxies list) 

1198 :rtype: :obj:`tuple` 

1199 """ 

1200 try: 

1201 proxies = [] 

1202 if self.allow_proxy_ticket: 

1203 ticket = models.Ticket.get(self.ticket, self.renew) 

1204 else: 

1205 ticket = models.ServiceTicket.get(self.ticket, self.renew) 

1206 try: 

1207 for prox in ticket.proxies.all(): 

1208 proxies.append(prox.url) 

1209 except AttributeError: 

1210 pass 

1211 if ticket.service != self.service: 

1212 raise ValidateError(u'INVALID_SERVICE', self.service) 

1213 return ticket, proxies 

1214 except Ticket.DoesNotExist: 

1215 raise ValidateError(u'INVALID_TICKET', self.ticket) 

1216 except (ServiceTicket.DoesNotExist, ProxyTicket.DoesNotExist): 

1217 raise ValidateError(u'INVALID_TICKET', 'ticket not found') 

1218 

1219 def process_pgturl(self, params): 

1220 """ 

1221 Handle PGT request 

1222 

1223 :param dict params: A template context dict 

1224 :raises ValidateError: if pgtUrl is invalid or if TLS validation of the pgtUrl fails 

1225 :return: The rendering of ``cas_server/serviceValidate.xml``, using ``params`` 

1226 :rtype: django.http.HttpResponse 

1227 """ 

1228 try: 

1229 pattern = ServicePattern.validate(self.pgt_url) 

1230 if pattern.proxy_callback: 

1231 proxyid = utils.gen_pgtiou() 

1232 pticket = ProxyGrantingTicket.objects.create( 

1233 user=self.ticket.user, 

1234 service=self.pgt_url, 

1235 service_pattern=pattern, 

1236 single_log_out=pattern.single_log_out 

1237 ) 

1238 url = utils.update_url(self.pgt_url, {'pgtIou': proxyid, 'pgtId': pticket.value}) 

1239 try: 

1240 ret = requests.get(url, verify=settings.CAS_PROXY_CA_CERTIFICATE_PATH) 

1241 if ret.status_code == 200: 

1242 params['proxyGrantingTicket'] = proxyid 

1243 else: 

1244 pticket.delete() 

1245 logger.info( 

1246 ( 

1247 "ValidateService: ticket %s validated for user %s on service %s. " 

1248 "Proxy Granting Ticket transmited to %s." 

1249 ) % ( 

1250 self.ticket.value, 

1251 self.ticket.user.username, 

1252 self.ticket.service, 

1253 self.pgt_url 

1254 ) 

1255 ) 

1256 logger.debug( 

1257 "ValidateService: User attributs are:\n%s" % ( 

1258 pprint.pformat(self.ticket.attributs), 

1259 ) 

1260 ) 

1261 return render( 

1262 self.request, 

1263 "cas_server/serviceValidate.xml", 

1264 params, 

1265 content_type="text/xml; charset=utf-8" 

1266 ) 

1267 except requests.exceptions.RequestException as error: 

1268 error = utils.unpack_nested_exception(error) 

1269 raise ValidateError( 

1270 u'INVALID_PROXY_CALLBACK', 

1271 u"%s: %s" % (type(error), str(error)) 

1272 ) 

1273 else: 

1274 raise ValidateError( 

1275 u'INVALID_PROXY_CALLBACK', 

1276 u"callback url not allowed by configuration" 

1277 ) 

1278 except ServicePattern.DoesNotExist: 

1279 raise ValidateError( 

1280 u'INVALID_PROXY_CALLBACK', 

1281 u'callback url not allowed by configuration' 

1282 ) 

1283 

1284 

1285class Proxy(View): 

1286 """proxy ticket service""" 

1287 

1288 #: Current :class:`django.http.HttpRequest` object 

1289 request = None 

1290 #: A ProxyGrantingTicket from the pgt GET parameter 

1291 pgt = None 

1292 #: the targetService GET parameter 

1293 target_service = None 

1294 

1295 def get(self, request): 

1296 """ 

1297 method called on GET request on this view 

1298 

1299 :param django.http.HttpRequest request: The current request object: 

1300 :return: The returned value of :meth:`process_proxy` if no error is raised, 

1301 else the rendering of ``cas_server/serviceValidateError.xml``. 

1302 :rtype: django.http.HttpResponse 

1303 """ 

1304 self.request = request 

1305 self.pgt = request.GET.get('pgt') 

1306 self.target_service = request.GET.get('targetService') 

1307 try: 

1308 # pgt and targetService parameters are mandatory 

1309 if self.pgt and self.target_service: 

1310 return self.process_proxy() 

1311 else: 

1312 raise ValidateError( 

1313 u'INVALID_REQUEST', 

1314 u"you must specify and pgt and targetService" 

1315 ) 

1316 except ValidateError as error: 

1317 logger.warning("Proxy: validation error: %s %s" % (error.code, error.msg)) 

1318 return error.render(request) 

1319 

1320 def process_proxy(self): 

1321 """ 

1322 handle PT request 

1323 

1324 :raises ValidateError: if the PGT is not found, or the target service not allowed or 

1325 the user not allowed on the tardet service. 

1326 :return: The rendering of ``cas_server/proxy.xml`` 

1327 :rtype: django.http.HttpResponse 

1328 """ 

1329 try: 

1330 # is the target service allowed 

1331 pattern = ServicePattern.validate(self.target_service) 

1332 # to get a proxy ticket require that the service allow it 

1333 if not pattern.proxy: 

1334 raise ValidateError( 

1335 u'UNAUTHORIZED_SERVICE', 

1336 u'the service %s does not allow proxy tickets' % self.target_service 

1337 ) 

1338 # is the proxy granting ticket valid 

1339 ticket = ProxyGrantingTicket.get(self.pgt) 

1340 # is the pgt user allowed on the target service 

1341 pattern.check_user(ticket.user) 

1342 pticket = ticket.user.get_ticket( 

1343 ProxyTicket, 

1344 self.target_service, 

1345 pattern, 

1346 renew=False 

1347 ) 

1348 models.Proxy.objects.create(proxy_ticket=pticket, url=ticket.service) 

1349 logger.info( 

1350 "Proxy ticket created for user %s on service %s." % ( 

1351 ticket.user.username, 

1352 self.target_service 

1353 ) 

1354 ) 

1355 return render( 

1356 self.request, 

1357 "cas_server/proxy.xml", 

1358 {'ticket': pticket.value}, 

1359 content_type="text/xml; charset=utf-8" 

1360 ) 

1361 except (Ticket.DoesNotExist, ProxyGrantingTicket.DoesNotExist): 

1362 raise ValidateError(u'INVALID_TICKET', u'PGT %s not found' % self.pgt) 

1363 except ServicePattern.DoesNotExist: 

1364 raise ValidateError(u'UNAUTHORIZED_SERVICE', self.target_service) 

1365 except (models.BadUsername, models.BadFilter, models.UserFieldNotDefined): 

1366 raise ValidateError( 

1367 u'UNAUTHORIZED_USER', 

1368 u'User %s not allowed on %s' % (ticket.user.username, self.target_service) 

1369 ) 

1370 

1371 

1372class SamlValidateError(ValidationBaseError): 

1373 """handle saml validation error""" 

1374 

1375 #: template to be render for the error 

1376 template = "cas_server/samlValidateError.xml" 

1377 

1378 def context(self): 

1379 """ 

1380 :return: A dictionary to contextualize :attr:`template` 

1381 :rtype: dict 

1382 """ 

1383 return { 

1384 'code': self.code, 

1385 'msg': self.msg, 

1386 'IssueInstant': timezone.now().isoformat(), 

1387 'ResponseID': utils.gen_saml_id() 

1388 } 

1389 

1390 

1391class SamlValidate(CsrfExemptView): 

1392 """SAML ticket validation""" 

1393 request = None 

1394 target = None 

1395 ticket = None 

1396 root = None 

1397 

1398 def post(self, request): 

1399 """ 

1400 method called on POST request on this view 

1401 

1402 :param django.http.HttpRequest request: The current request object 

1403 :return: the rendering of ``cas_server/samlValidate.xml`` if no error is raised, 

1404 else the rendering of ``cas_server/samlValidateError.xml``. 

1405 :rtype: django.http.HttpResponse 

1406 """ 

1407 self.request = request 

1408 self.target = request.GET.get('TARGET') 

1409 self.root = etree.fromstring(request.body) 

1410 try: 

1411 self.ticket = self.process_ticket() 

1412 expire_instant = (self.ticket.creation + 

1413 timedelta(seconds=self.ticket.VALIDITY)).isoformat() 

1414 params = { 

1415 'IssueInstant': timezone.now().isoformat(), 

1416 'expireInstant': expire_instant, 

1417 'Recipient': self.target, 

1418 'ResponseID': utils.gen_saml_id(), 

1419 'username': self.ticket.username(), 

1420 'attributes': self.ticket.attributs_flat(), 

1421 'auth_date': self.ticket.user.last_login.replace(microsecond=0).isoformat(), 

1422 'is_new_login': 'true' if self.ticket.renew else 'false' 

1423 

1424 } 

1425 logger.info( 

1426 "SamlValidate: ticket %s validated for user %s on service %s." % ( 

1427 self.ticket.value, 

1428 self.ticket.user.username, 

1429 self.ticket.service 

1430 ) 

1431 ) 

1432 logger.debug( 

1433 "SamlValidate: User attributes are:\n%s" % pprint.pformat(self.ticket.attributs) 

1434 ) 

1435 

1436 return render( 

1437 request, 

1438 "cas_server/samlValidate.xml", 

1439 params, 

1440 content_type="text/xml; charset=utf-8" 

1441 ) 

1442 except SamlValidateError as error: 

1443 logger.warning("SamlValidate: validation error: %s %s" % (error.code, error.msg)) 

1444 return error.render(request) 

1445 

1446 def process_ticket(self): 

1447 """ 

1448 validate ticket from SAML XML body 

1449 

1450 :raises: SamlValidateError: if the ticket is not found or not valid, or if we fail 

1451 to parse the posted XML. 

1452 :return: a ticket object 

1453 :rtype: :class:`models.Ticket<cas_server.models.Ticket>` 

1454 """ 

1455 try: 

1456 auth_req = self.root.getchildren()[1].getchildren()[0] 

1457 ticket = auth_req.getchildren()[0].text 

1458 ticket = models.Ticket.get(ticket) 

1459 if ticket.service != self.target: 

1460 raise SamlValidateError( 

1461 u'AuthnFailed', 

1462 u'TARGET %s does not match ticket service' % self.target 

1463 ) 

1464 return ticket 

1465 except (IndexError, KeyError): 

1466 raise SamlValidateError(u'VersionMismatch') 

1467 except Ticket.DoesNotExist: 

1468 raise SamlValidateError( 

1469 u'AuthnFailed', 

1470 u'ticket %s should begin with PT- or ST-' % ticket 

1471 ) 

1472 except (ServiceTicket.DoesNotExist, ProxyTicket.DoesNotExist): 

1473 raise SamlValidateError(u'AuthnFailed', u'ticket %s not found' % ticket)