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 import timezone 

20from django.views.decorators.csrf import csrf_exempt 

21from django.middleware.csrf import CsrfViewMiddleware 

22from django.views.generic import View 

23try: 

24 from django.utils.encoding import python_2_unicode_compatible 

25 from django.utils.translation import ugettext as _ 

26except ImportError: 

27 def python_2_unicode_compatible(func): 

28 """ 

29 We use Django >= 3.0 with Python >= 3.4, we don't need Python 2 compatibility. 

30 """ 

31 return func 

32 from django.utils.translation import gettext as _ 

33from django.utils.safestring import mark_safe 

34try: 

35 from django.urls import reverse 

36except ImportError: 

37 from django.core.urlresolvers import reverse 

38 

39import re 

40import logging 

41import pprint 

42import requests 

43from lxml import etree 

44from datetime import timedelta 

45 

46import cas_server.utils as utils 

47import cas_server.forms as forms 

48import cas_server.models as models 

49 

50from .utils import json_response 

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

52from .models import ServicePattern, FederatedIendityProvider, FederatedUser 

53from .federate import CASFederateValidateUser 

54 

55logger = logging.getLogger(__name__) 

56 

57 

58class LogoutMixin(object): 

59 """destroy CAS session utils""" 

60 

61 def logout(self, all_session=False): 

62 """ 

63 effectively destroy a CAS session 

64 

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

66 destroy the current user session. 

67 :return: The number of destroyed sessions 

68 :rtype: int 

69 """ 

70 # initialize the counter of the number of destroyed sesisons 

71 session_nb = 0 

72 # save the current user username before flushing the session 

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

74 if username: 

75 if all_session: 

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

77 else: 

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

79 users = [] 

80 # try to get the user from the current session 

81 try: 

82 users.append( 

83 models.User.objects.get( 

84 username=username, 

85 session_key=self.request.session.session_key 

86 ) 

87 ) 

88 except models.User.DoesNotExist: 

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

90 self.request.session.flush() 

91 

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

93 if all_session: 

94 users.extend( 

95 models.User.objects.filter( 

96 username=username 

97 ).exclude( 

98 session_key=self.request.session.session_key 

99 ) 

100 ) 

101 

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

103 for user in users: 

104 # get the user session 

105 session = SessionStore(session_key=user.session_key) 

106 # flush the session 

107 session.flush() 

108 # send SLO requests 

109 user.logout(self.request) 

110 # delete the user 

111 user.delete() 

112 # increment the destroyed session counter 

113 session_nb += 1 

114 if username: 

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

116 return session_nb 

117 

118 

119class CsrfExemptView(View): 

120 """base class for csrf exempt class views""" 

121 

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

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

124 """ 

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

126 

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

128 """ 

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

130 

131 

132class LogoutView(View, LogoutMixin): 

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

134 

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

136 request = None 

137 #: service GET parameter 

138 service = None 

139 #: url GET paramet 

140 url = None 

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

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

143 ajax = None 

144 

145 def init_get(self, request): 

146 """ 

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

148 

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

150 """ 

151 self.request = request 

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

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

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

155 

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

157 """ 

158 method called on GET request on this view 

159 

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

161 """ 

162 logger.info("logout requested") 

163 # initialize the class attributes 

164 self.init_get(request) 

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

166 if settings.CAS_FEDERATE: 

167 try: 

168 user = FederatedUser.get_from_federated_username( 

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

170 ) 

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

172 except FederatedUser.DoesNotExist: 

173 auth = None 

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

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

176 # current querystring 

177 if settings.CAS_FEDERATE: 

178 if auth is not None: 

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

180 url = auth.get_logout_url() 

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

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

183 response.delete_cookie("remember_provider") 

184 return response 

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

186 if self.service: 

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

188 return HttpResponseRedirect(self.service) 

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

190 elif self.url: 

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

192 return HttpResponseRedirect(self.url) 

193 else: 

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

195 if session_nb == 1: 

196 logout_msg = mark_safe(_( 

197 "<h3>Logout successful</h3>" 

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

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

200 )) 

201 elif session_nb > 1: 

202 logout_msg = mark_safe(_( 

203 "<h3>Logout successful</h3>" 

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

205 "Authentication Service. " 

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

207 ) % session_nb) 

208 else: 

209 logout_msg = mark_safe(_( 

210 "<h3>Logout successful</h3>" 

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

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

213 )) 

214 

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

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

217 if settings.CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT: 

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

219 if self.ajax: 

220 url = reverse("cas_server:login") 

221 data = { 

222 'status': 'success', 

223 'detail': 'logout', 

224 'url': url, 

225 'session_nb': session_nb 

226 } 

227 return json_response(request, data) 

228 else: 

229 return redirect("cas_server:login") 

230 else: 

231 if self.ajax: 

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

233 return json_response(request, data) 

234 else: 

235 return render( 

236 request, 

237 settings.CAS_LOGOUT_TEMPLATE, 

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

239 ) 

240 

241 

242class FederateAuth(CsrfExemptView): 

243 """ 

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

245 

246 csrf is disabled for allowing SLO requests reception. 

247 """ 

248 

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

250 service_url = None 

251 

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

253 """ 

254 return a CAS client object matching provider 

255 

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

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

258 :return: The user CAS client object 

259 :rtype: :class:`federate.CASFederateValidateUser 

260 <cas_server.federate.CASFederateValidateUser>` 

261 """ 

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

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

264 self.service_url = service_url 

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

266 

267 def post(self, request, provider=None, *args, **kwargs): 

268 """ 

269 method called on POST request 

270 

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

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

273 """ 

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

275 if not settings.CAS_FEDERATE: 

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

277 return redirect("cas_server:login") 

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

279 # allowing SLO requests reception 

280 try: 

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

282 auth = self.get_cas_client(request, provider) 

283 try: 

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

285 except (KeyError, AttributeError): 

286 pass 

287 return HttpResponse("ok") 

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

289 except FederatedIendityProvider.DoesNotExist: 

290 # Manually checking for csrf to protect the code below 

291 reason = CsrfViewMiddleware(lambda request: HttpResponse()) \ 

292 .process_view(request, None, (), {}) 

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

294 return reason # Failed the test, stop here. 

295 form = forms.FederateSelect(request.POST) 

296 if form.is_valid(): 

297 params = utils.copy_params( 

298 request.POST, 

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

300 ) 

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

302 del params["renew"] 

303 url = utils.reverse_params( 

304 "cas_server:federateAuth", 

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

306 params=params 

307 ) 

308 return HttpResponseRedirect(url) 

309 else: 

310 return redirect("cas_server:login") 

311 

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

313 """ 

314 method called on GET request 

315 

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

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

318 """ 

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

320 if not settings.CAS_FEDERATE: 

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

322 return redirect("cas_server:login") 

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

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

325 # identity provider. 

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

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

328 return redirect("cas_server:login") 

329 try: 

330 # get the identity provider from its suffix 

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

332 # get a CAS client for the user identity provider 

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

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

335 if 'ticket' not in request.GET: 

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

337 return HttpResponseRedirect(auth.get_login_url()) 

338 else: 

339 ticket = request.GET['ticket'] 

340 try: 

341 # if the ticket validation succeed 

342 if auth.verify_ticket(ticket): 

343 logger.info( 

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

345 auth.username, 

346 auth.provider.server_url 

347 ) 

348 ) 

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

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

351 request.session["federate_ticket"] = ticket 

352 auth.register_slo( 

353 auth.federated_username, 

354 request.session.session_key, 

355 ticket 

356 ) 

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

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

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

360 response = HttpResponseRedirect(url) 

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

362 # cookie 

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

364 max_age = settings.CAS_FEDERATE_REMEMBER_TIMEOUT 

365 utils.set_cookie( 

366 response, 

367 "remember_provider", 

368 provider.suffix, 

369 max_age 

370 ) 

371 return response 

372 # else redirect to the identity provider CAS login page 

373 else: 

374 logger.info( 

375 ( 

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

377 "Retrying authentication" 

378 ) % ( 

379 ticket, 

380 auth.provider.server_url, 

381 self.service_url 

382 ) 

383 ) 

384 return HttpResponseRedirect(auth.get_login_url()) 

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

386 except SyntaxError as error: 

387 messages.add_message( 

388 request, 

389 messages.ERROR, 

390 _( 

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

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

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

394 ) 

395 response = redirect("cas_server:login") 

396 response.delete_cookie("remember_provider") 

397 return response 

398 except FederatedIendityProvider.DoesNotExist: 

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

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

401 return redirect("cas_server:login") 

402 

403 

404class LoginView(View, LogoutMixin): 

405 """credential requestor / acceptor""" 

406 

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

408 # Nine is reasonable in this case. 

409 

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

411 user = None 

412 #: The form to display to the user 

413 form = None 

414 

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

416 request = None 

417 #: service GET/POST parameter 

418 service = None 

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

420 renew = None 

421 #: the warn GET/POST parameter 

422 warn = None 

423 #: the gateway GET/POST parameter 

424 gateway = None 

425 #: the method GET/POST parameter 

426 method = None 

427 

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

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

430 ajax = None 

431 

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

433 renewed = False 

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

435 warned = False 

436 

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

438 #: is ``True``) 

439 username = None 

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

441 #: ``True``) 

442 ticket = None 

443 

444 INVALID_LOGIN_TICKET = 1 

445 USER_LOGIN_OK = 2 

446 USER_LOGIN_FAILURE = 3 

447 USER_ALREADY_LOGGED = 4 

448 USER_AUTHENTICATED = 5 

449 USER_NOT_AUTHENTICATED = 6 

450 

451 def init_post(self, request): 

452 """ 

453 Initialize POST received parameters 

454 

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

456 """ 

457 self.request = request 

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

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

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

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

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

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

464 self.warned = True 

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

466 if settings.CAS_FEDERATE: 

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

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

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

470 

471 def gen_lt(self): 

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

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

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

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

476 

477 def check_lt(self): 

478 """ 

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

480 

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

482 :rtype: bool 

483 """ 

484 # save LT for later check 

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

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

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

488 self.gen_lt() 

489 # check if send LT is valid 

490 if lt_send not in lt_valid: 

491 return False 

492 else: 

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

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

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

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

497 return True 

498 

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

500 """ 

501 method called on POST request on this view 

502 

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

504 """ 

505 # initialize class parameters 

506 self.init_post(request) 

507 # process the POST request 

508 ret = self.process_post() 

509 if ret == self.INVALID_LOGIN_TICKET: 

510 messages.add_message( 

511 self.request, 

512 messages.ERROR, 

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

514 ) 

515 elif ret == self.USER_LOGIN_OK: 

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

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

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

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

520 session_key=self.request.session.session_key 

521 )[0] 

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

523 self.user.save() 

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

525 if settings.CAS_FEDERATE: 

526 self.ticket = None 

527 self.username = None 

528 self.init_form() 

529 # preserve valid LoginTickets from session flush 

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

531 # On login failure, flush the session 

532 self.logout() 

533 # restore valid LoginTickets 

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

535 elif ret == self.USER_ALREADY_LOGGED: 

536 pass 

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

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

539 # call the GET/POST common part 

540 response = self.common() 

541 if self.warn: 

542 utils.set_cookie( 

543 response, 

544 "warn", 

545 "on", 

546 10 * 365 * 24 * 3600 

547 ) 

548 else: 

549 response.delete_cookie("warn") 

550 return response 

551 

552 def process_post(self): 

553 """ 

554 Analyse the POST request: 

555 

556 * check that the LoginTicket is valid 

557 * check that the user sumited credentials are valid 

558 

559 :return: 

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

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

562 reauthentication. 

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

564 reauthentication and his credentials are not valid 

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

566 reauthentication and his credentials are valid 

567 :rtype: int 

568 """ 

569 if not self.check_lt(): 

570 self.init_form(self.request.POST) 

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

572 return self.INVALID_LOGIN_TICKET 

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

574 # authentication request receive, initialize the form to use 

575 self.init_form(self.request.POST) 

576 if self.form.is_valid(): 

577 self.request.session.set_expiry(0) 

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

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

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

581 self.renewed = True 

582 self.warned = True 

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

584 return self.USER_LOGIN_OK 

585 else: 

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

587 return self.USER_LOGIN_FAILURE 

588 else: 

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

590 return self.USER_ALREADY_LOGGED 

591 

592 def init_get(self, request): 

593 """ 

594 Initialize GET received parameters 

595 

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

597 """ 

598 self.request = request 

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

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

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

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

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

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

605 if settings.CAS_FEDERATE: 

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

607 # FederateAuth.get 

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

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

610 if self.username: 

611 del request.session["federate_username"] 

612 if self.ticket: 

613 del request.session["federate_ticket"] 

614 

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

616 """ 

617 method called on GET request on this view 

618 

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

620 """ 

621 # initialize class parameters 

622 self.init_get(request) 

623 # process the GET request 

624 self.process_get() 

625 # call the GET/POST common part 

626 return self.common() 

627 

628 def process_get(self): 

629 """ 

630 Analyse the GET request 

631 

632 :return: 

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

634 for authentication renewal 

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

636 for authentication renewal 

637 :rtype: int 

638 """ 

639 # generate a new LT 

640 self.gen_lt() 

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

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

643 self.init_form() 

644 return self.USER_NOT_AUTHENTICATED 

645 return self.USER_AUTHENTICATED 

646 

647 def init_form(self, values=None): 

648 """ 

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

650 

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

652 """ 

653 if values: 

654 values = values.copy() 

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

656 form_initial = { 

657 'service': self.service, 

658 'method': self.method, 

659 'warn': ( 

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

661 ), 

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

663 'renew': self.renew 

664 } 

665 if settings.CAS_FEDERATE: 

666 if self.username and self.ticket: 

667 form_initial['username'] = self.username 

668 form_initial['password'] = self.ticket 

669 form_initial['ticket'] = self.ticket 

670 self.form = forms.FederateUserCredential( 

671 values, 

672 initial=form_initial 

673 ) 

674 else: 

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

676 else: 

677 self.form = forms.UserCredential( 

678 values, 

679 initial=form_initial 

680 ) 

681 

682 def service_login(self): 

683 """ 

684 Perform login against a service 

685 

686 :return: 

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

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

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

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

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

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

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

694 :rtype: django.http.HttpResponse 

695 """ 

696 try: 

697 # is the service allowed 

698 service_pattern = ServicePattern.validate(self.service) 

699 # is the current user allowed on this service 

700 service_pattern.check_user(self.user) 

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

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

703 messages.add_message( 

704 self.request, 

705 messages.WARNING, 

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

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

708 ) 

709 if self.ajax: 

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

711 return json_response(self.request, data) 

712 else: 

713 warn_form = forms.WarnForm(initial={ 

714 'service': self.service, 

715 'renew': self.renew, 

716 'gateway': self.gateway, 

717 'method': self.method, 

718 'warned': True, 

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

720 }) 

721 return render( 

722 self.request, 

723 settings.CAS_WARN_TEMPLATE, 

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

725 ) 

726 else: 

727 # redirect, using method ? 

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

729 redirect_url = self.user.get_service_url( 

730 self.service, 

731 service_pattern, 

732 renew=self.renewed 

733 ) 

734 if not self.ajax: 

735 return HttpResponseRedirect(redirect_url) 

736 else: 

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

738 return json_response(self.request, data) 

739 except ServicePattern.DoesNotExist: 

740 error = 1 

741 messages.add_message( 

742 self.request, 

743 messages.ERROR, 

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

745 ) 

746 except models.BadUsername: 

747 error = 2 

748 messages.add_message( 

749 self.request, 

750 messages.ERROR, 

751 _(u"Username not allowed") 

752 ) 

753 except models.BadFilter: 

754 error = 3 

755 messages.add_message( 

756 self.request, 

757 messages.ERROR, 

758 _(u"User characteristics not allowed") 

759 ) 

760 except models.UserFieldNotDefined: 

761 error = 4 

762 messages.add_message( 

763 self.request, 

764 messages.ERROR, 

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

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

767 ) 

768 

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

770 if self.gateway and not self.ajax: 

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

772 return HttpResponseRedirect(self.service) 

773 

774 if not self.ajax: 

775 return render( 

776 self.request, 

777 settings.CAS_LOGGED_TEMPLATE, 

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

779 ) 

780 else: 

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

782 return json_response(self.request, data) 

783 

784 def authenticated(self): 

785 """ 

786 Processing authenticated users 

787 

788 :return: 

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

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

791 :rtype: django.http.HttpResponse 

792 """ 

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

794 # session 

795 try: 

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

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

798 session_key=self.request.session.session_key 

799 ) 

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

801 except models.User.DoesNotExist: 

802 logger.warning( 

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

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

805 ) 

806 ) 

807 self.logout() 

808 if self.ajax: 

809 data = { 

810 "status": "error", 

811 "detail": "login required", 

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

813 } 

814 return json_response(self.request, data) 

815 else: 

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

817 

818 # if login against a service 

819 if self.service: 

820 return self.service_login() 

821 # else display the logged template 

822 else: 

823 if self.ajax: 

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

825 return json_response(self.request, data) 

826 else: 

827 return render( 

828 self.request, 

829 settings.CAS_LOGGED_TEMPLATE, 

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

831 ) 

832 

833 def not_authenticated(self): 

834 """ 

835 Processing non authenticated users 

836 

837 :return: 

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

839 depending of GET/POST parameters 

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

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

842 :rtype: django.http.HttpResponse 

843 """ 

844 if self.service: 

845 try: 

846 service_pattern = ServicePattern.validate(self.service) 

847 if self.gateway and not self.ajax: 

848 # clean messages before leaving django 

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

850 return HttpResponseRedirect(self.service) 

851 

852 if settings.CAS_SHOW_SERVICE_MESSAGES: 

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

854 messages.add_message( 

855 self.request, 

856 messages.WARNING, 

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

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

859 ) 

860 else: 

861 messages.add_message( 

862 self.request, 

863 messages.WARNING, 

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

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

866 ) 

867 except ServicePattern.DoesNotExist: 

868 if settings.CAS_SHOW_SERVICE_MESSAGES: 

869 messages.add_message( 

870 self.request, 

871 messages.ERROR, 

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

873 ) 

874 if self.ajax: 

875 data = { 

876 "status": "error", 

877 "detail": "login required", 

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

879 } 

880 return json_response(self.request, data) 

881 else: 

882 if settings.CAS_FEDERATE: 

883 if self.username and self.ticket: 

884 return render( 

885 self.request, 

886 settings.CAS_LOGIN_TEMPLATE, 

887 utils.context({ 

888 'form': self.form, 

889 'auto_submit': True, 

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

891 }) 

892 ) 

893 else: 

894 if ( 

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

896 FederatedIendityProvider.objects.filter( 

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

898 ) 

899 ): 

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

901 url = utils.reverse_params( 

902 "cas_server:federateAuth", 

903 params=params, 

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

905 ) 

906 return HttpResponseRedirect(url) 

907 else: 

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

909 # to the user identity provider 

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

911 try: 

912 user = FederatedUser.get_from_federated_username( 

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

914 ) 

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

916 url = utils.reverse_params( 

917 "cas_server:federateAuth", 

918 params=params, 

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

920 ) 

921 return HttpResponseRedirect(url) 

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

923 # database. 

924 except FederatedUser.DoesNotExist: # pragma: no cover 

925 pass 

926 return render( 

927 self.request, 

928 settings.CAS_LOGIN_TEMPLATE, 

929 utils.context({ 

930 'form': self.form, 

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

932 }) 

933 ) 

934 else: 

935 return render( 

936 self.request, 

937 settings.CAS_LOGIN_TEMPLATE, 

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

939 ) 

940 

941 def common(self): 

942 """ 

943 Common part execute uppon GET and POST request 

944 

945 :return: 

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

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

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

949 :rtype: django.http.HttpResponse 

950 """ 

951 # if authenticated and successfully renewed authentication if needed 

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

953 return self.authenticated() 

954 else: 

955 return self.not_authenticated() 

956 

957 

958class Auth(CsrfExemptView): 

959 """ 

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

961 

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

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

964 """ 

965 

966 @staticmethod 

967 def post(request): 

968 """ 

969 method called on POST request on this view 

970 

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

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

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

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

975 line. 

976 :rtype: django.http.HttpResponse 

977 """ 

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

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

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

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

982 

983 if not settings.CAS_AUTH_SHARED_SECRET: 

984 return HttpResponse( 

985 "no\nplease set CAS_AUTH_SHARED_SECRET", 

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

987 ) 

988 if secret != settings.CAS_AUTH_SHARED_SECRET: 

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

990 if not username or not password or not service: 

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

992 form = forms.UserCredential( 

993 request.POST, 

994 initial={ 

995 'service': service, 

996 'method': 'POST', 

997 'warn': False 

998 } 

999 ) 

1000 if form.is_valid(): 

1001 try: 

1002 user = models.User.objects.get_or_create( 

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

1004 session_key=request.session.session_key 

1005 )[0] 

1006 user.save() 

1007 # is the service allowed 

1008 service_pattern = ServicePattern.validate(service) 

1009 # is the current user allowed on this service 

1010 service_pattern.check_user(user) 

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

1012 user.delete() 

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

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

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

1016 else: 

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

1018 

1019 

1020class Validate(View): 

1021 """service ticket validation""" 

1022 @staticmethod 

1023 def get(request): 

1024 """ 

1025 method called on GET request on this view 

1026 

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

1028 :return: 

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

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

1031 :rtype: django.http.HttpResponse 

1032 """ 

1033 # store wanted GET parameters 

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

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

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

1037 # service and ticket parameters are mandatory 

1038 if service and ticket: 

1039 try: 

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

1041 # still valid 

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

1043 logger.info( 

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

1045 ticket.value, 

1046 ticket.user.username, 

1047 ticket.service 

1048 ) 

1049 ) 

1050 return HttpResponse( 

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

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

1053 ) 

1054 except ServiceTicket.DoesNotExist: 

1055 logger.warning( 

1056 ( 

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

1058 "already validated, auth to %s failed" 

1059 ) % ( 

1060 ticket, 

1061 service 

1062 ) 

1063 ) 

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

1065 else: 

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

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

1068 

1069 

1070@python_2_unicode_compatible 

1071class ValidationBaseError(Exception): 

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

1073 

1074 #: The error code 

1075 code = None 

1076 #: The error message 

1077 msg = None 

1078 

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

1080 self.code = code 

1081 self.msg = msg 

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

1083 

1084 def __str__(self): 

1085 return u"%s" % self.msg 

1086 

1087 def render(self, request): 

1088 """ 

1089 render the error template for the exception 

1090 

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

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

1093 :rtype: django.http.HttpResponse 

1094 """ 

1095 return render( 

1096 request, 

1097 self.template, 

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

1099 ) 

1100 

1101 

1102class ValidateError(ValidationBaseError): 

1103 """handle service validation error""" 

1104 

1105 #: template to be render for the error 

1106 template = "cas_server/serviceValidateError.xml" 

1107 

1108 def context(self): 

1109 """ 

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

1111 

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

1113 :rtype: dict 

1114 """ 

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

1116 

1117 

1118class ValidateService(View): 

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

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

1121 request = None 

1122 #: The service GET parameter 

1123 service = None 

1124 #: the ticket GET parameter 

1125 ticket = None 

1126 #: the pgtUrl GET parameter 

1127 pgt_url = None 

1128 #: the renew GET parameter 

1129 renew = None 

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

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

1132 allow_proxy_ticket = False 

1133 

1134 def get(self, request): 

1135 """ 

1136 method called on GET request on this view 

1137 

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

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

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

1141 :rtype: django.http.HttpResponse 

1142 """ 

1143 # define the class parameters 

1144 self.request = request 

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

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

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

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

1149 

1150 # service and ticket parameter are mandatory 

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

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

1153 return ValidateError( 

1154 u'INVALID_REQUEST', 

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

1156 ).render(request) 

1157 else: 

1158 try: 

1159 # search the ticket in the database 

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

1161 # prepare template rendering context 

1162 params = { 

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

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

1165 'proxies': proxies, 

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

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

1168 } 

1169 # if pgtUrl is set, require https or localhost 

1170 if self.pgt_url and ( 

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

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

1173 ): 

1174 return self.process_pgturl(params) 

1175 else: 

1176 logger.info( 

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

1178 self.ticket.value, 

1179 self.ticket.user.username, 

1180 self.ticket.service 

1181 ) 

1182 ) 

1183 logger.debug( 

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

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

1186 ) 

1187 ) 

1188 return render( 

1189 request, 

1190 "cas_server/serviceValidate.xml", 

1191 params, 

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

1193 ) 

1194 except ValidateError as error: 

1195 logger.warning( 

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

1197 ) 

1198 return error.render(request) 

1199 

1200 def process_ticket(self): 

1201 """ 

1202 fetch the ticket against the database and check its validity 

1203 

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

1205 service 

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

1207 :rtype: :obj:`tuple` 

1208 """ 

1209 try: 

1210 proxies = [] 

1211 if self.allow_proxy_ticket: 

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

1213 else: 

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

1215 try: 

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

1217 proxies.append(prox.url) 

1218 except AttributeError: 

1219 pass 

1220 if ticket.service != self.service: 

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

1222 return ticket, proxies 

1223 except Ticket.DoesNotExist: 

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

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

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

1227 

1228 def process_pgturl(self, params): 

1229 """ 

1230 Handle PGT request 

1231 

1232 :param dict params: A template context dict 

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

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

1235 :rtype: django.http.HttpResponse 

1236 """ 

1237 try: 

1238 pattern = ServicePattern.validate(self.pgt_url) 

1239 if pattern.proxy_callback: 

1240 proxyid = utils.gen_pgtiou() 

1241 pticket = ProxyGrantingTicket.objects.create( 

1242 user=self.ticket.user, 

1243 service=self.pgt_url, 

1244 service_pattern=pattern, 

1245 single_log_out=pattern.single_log_out 

1246 ) 

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

1248 try: 

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

1250 if ret.status_code == 200: 

1251 params['proxyGrantingTicket'] = proxyid 

1252 else: 

1253 pticket.delete() 

1254 logger.info( 

1255 ( 

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

1257 "Proxy Granting Ticket transmited to %s." 

1258 ) % ( 

1259 self.ticket.value, 

1260 self.ticket.user.username, 

1261 self.ticket.service, 

1262 self.pgt_url 

1263 ) 

1264 ) 

1265 logger.debug( 

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

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

1268 ) 

1269 ) 

1270 return render( 

1271 self.request, 

1272 "cas_server/serviceValidate.xml", 

1273 params, 

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

1275 ) 

1276 except requests.exceptions.RequestException as error: 

1277 error = utils.unpack_nested_exception(error) 

1278 raise ValidateError( 

1279 u'INVALID_PROXY_CALLBACK', 

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

1281 ) 

1282 else: 

1283 raise ValidateError( 

1284 u'INVALID_PROXY_CALLBACK', 

1285 u"callback url not allowed by configuration" 

1286 ) 

1287 except ServicePattern.DoesNotExist: 

1288 raise ValidateError( 

1289 u'INVALID_PROXY_CALLBACK', 

1290 u'callback url not allowed by configuration' 

1291 ) 

1292 

1293 

1294class Proxy(View): 

1295 """proxy ticket service""" 

1296 

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

1298 request = None 

1299 #: A ProxyGrantingTicket from the pgt GET parameter 

1300 pgt = None 

1301 #: the targetService GET parameter 

1302 target_service = None 

1303 

1304 def get(self, request): 

1305 """ 

1306 method called on GET request on this view 

1307 

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

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

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

1311 :rtype: django.http.HttpResponse 

1312 """ 

1313 self.request = request 

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

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

1316 try: 

1317 # pgt and targetService parameters are mandatory 

1318 if self.pgt and self.target_service: 

1319 return self.process_proxy() 

1320 else: 

1321 raise ValidateError( 

1322 u'INVALID_REQUEST', 

1323 u"you must specify and pgt and targetService" 

1324 ) 

1325 except ValidateError as error: 

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

1327 return error.render(request) 

1328 

1329 def process_proxy(self): 

1330 """ 

1331 handle PT request 

1332 

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

1334 the user not allowed on the tardet service. 

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

1336 :rtype: django.http.HttpResponse 

1337 """ 

1338 try: 

1339 # is the target service allowed 

1340 pattern = ServicePattern.validate(self.target_service) 

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

1342 if not pattern.proxy: 

1343 raise ValidateError( 

1344 u'UNAUTHORIZED_SERVICE', 

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

1346 ) 

1347 # is the proxy granting ticket valid 

1348 ticket = ProxyGrantingTicket.get(self.pgt) 

1349 # is the pgt user allowed on the target service 

1350 pattern.check_user(ticket.user) 

1351 pticket = ticket.user.get_ticket( 

1352 ProxyTicket, 

1353 self.target_service, 

1354 pattern, 

1355 renew=False 

1356 ) 

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

1358 logger.info( 

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

1360 ticket.user.username, 

1361 self.target_service 

1362 ) 

1363 ) 

1364 return render( 

1365 self.request, 

1366 "cas_server/proxy.xml", 

1367 {'ticket': pticket.value}, 

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

1369 ) 

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

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

1372 except ServicePattern.DoesNotExist: 

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

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

1375 raise ValidateError( 

1376 u'UNAUTHORIZED_USER', 

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

1378 ) 

1379 

1380 

1381class SamlValidateError(ValidationBaseError): 

1382 """handle saml validation error""" 

1383 

1384 #: template to be render for the error 

1385 template = "cas_server/samlValidateError.xml" 

1386 

1387 def context(self): 

1388 """ 

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

1390 :rtype: dict 

1391 """ 

1392 return { 

1393 'code': self.code, 

1394 'msg': self.msg, 

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

1396 'ResponseID': utils.gen_saml_id() 

1397 } 

1398 

1399 

1400class SamlValidate(CsrfExemptView): 

1401 """SAML ticket validation""" 

1402 request = None 

1403 target = None 

1404 ticket = None 

1405 root = None 

1406 

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

1408 """ 

1409 method called on POST request on this view 

1410 

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

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

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

1414 :rtype: django.http.HttpResponse 

1415 """ 

1416 self.request = request 

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

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

1419 try: 

1420 self.ticket = self.process_ticket() 

1421 expire_instant = (self.ticket.creation + 

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

1423 params = { 

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

1425 'expireInstant': expire_instant, 

1426 'Recipient': self.target, 

1427 'ResponseID': utils.gen_saml_id(), 

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

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

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

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

1432 

1433 } 

1434 logger.info( 

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

1436 self.ticket.value, 

1437 self.ticket.user.username, 

1438 self.ticket.service 

1439 ) 

1440 ) 

1441 logger.debug( 

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

1443 ) 

1444 

1445 return render( 

1446 request, 

1447 "cas_server/samlValidate.xml", 

1448 params, 

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

1450 ) 

1451 except SamlValidateError as error: 

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

1453 return error.render(request) 

1454 

1455 def process_ticket(self): 

1456 """ 

1457 validate ticket from SAML XML body 

1458 

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

1460 to parse the posted XML. 

1461 :return: a ticket object 

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

1463 """ 

1464 try: 

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

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

1467 ticket = models.Ticket.get(ticket) 

1468 if ticket.service != self.target: 

1469 raise SamlValidateError( 

1470 u'AuthnFailed', 

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

1472 ) 

1473 return ticket 

1474 except (IndexError, KeyError): 

1475 raise SamlValidateError(u'VersionMismatch') 

1476 except Ticket.DoesNotExist: 

1477 raise SamlValidateError( 

1478 u'AuthnFailed', 

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

1480 ) 

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

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