Coverage for cas_server/views.py: 99%

594 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-08-18 11:22 +0000

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.models as models 

48 

49from .utils import json_response, import_attr 

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

51from .models import ServicePattern, FederatedIendityProvider, FederatedUser 

52from .federate import CASFederateValidateUser 

53 

54logger = logging.getLogger(__name__) 

55 

56 

57class LogoutMixin(object): 

58 """destroy CAS session utils""" 

59 

60 def logout(self, all_session=False): 

61 """ 

62 effectively destroy a CAS session 

63 

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

65 destroy the current user session. 

66 :return: The number of destroyed sessions 

67 :rtype: int 

68 """ 

69 # initialize the counter of the number of destroyed sesisons 

70 session_nb = 0 

71 # save the current user username before flushing the session 

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

73 if username: 

74 if all_session: 

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

76 else: 

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

78 users = [] 

79 # try to get the user from the current session 

80 try: 

81 users.append( 

82 models.User.objects.get( 

83 username=username, 

84 session_key=self.request.session.session_key 

85 ) 

86 ) 

87 except models.User.DoesNotExist: 

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

89 self.request.session.flush() 

90 

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

92 if all_session: 

93 users.extend( 

94 models.User.objects.filter( 

95 username=username 

96 ).exclude( 

97 session_key=self.request.session.session_key 

98 ) 

99 ) 

100 

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

102 for user in users: 

103 # get the user session 

104 session = SessionStore(session_key=user.session_key) 

105 # flush the session 

106 session.flush() 

107 # send SLO requests 

108 user.logout(self.request) 

109 # delete the user 

110 user.delete() 

111 # increment the destroyed session counter 

112 session_nb += 1 

113 if username: 

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

115 return session_nb 

116 

117 

118class CsrfExemptView(View): 

119 """base class for csrf exempt class views""" 

120 

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

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

123 """ 

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

125 

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

127 """ 

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

129 

130 

131class LogoutView(View, LogoutMixin): 

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

133 

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

135 request = None 

136 #: service GET parameter 

137 service = None 

138 #: url GET paramet 

139 url = None 

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

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

142 ajax = None 

143 

144 def init_get(self, request): 

145 """ 

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

147 

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

149 """ 

150 self.request = request 

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

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

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

154 

155 @staticmethod 

156 def delete_cookies(response): 

157 if settings.CAS_REMOVE_DJANGO_SESSION_COOKIE_ON_LOGOUT: 157 ↛ 158line 157 didn't jump to line 158 because the condition on line 157 was never true

158 response.delete_cookie(settings.SESSION_COOKIE_NAME) 

159 if settings.CAS_REMOVE_DJANGO_CSRF_COOKIE_ON_LOGOUT: 159 ↛ 160line 159 didn't jump to line 160 because the condition on line 159 was never true

160 response.delete_cookie(settings.CSRF_COOKIE_NAME) 

161 if settings.CAS_REMOVE_DJANGO_LANGUAGE_COOKIE_ON_LOGOUT: 161 ↛ 162line 161 didn't jump to line 162 because the condition on line 161 was never true

162 response.delete_cookie(settings.LANGUAGE_COOKIE_NAME) 

163 return response 

164 

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

166 """ 

167 method called on GET request on this view 

168 

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

170 """ 

171 logger.info("logout requested") 

172 # initialize the class attributes 

173 self.init_get(request) 

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

175 if settings.CAS_FEDERATE: 

176 try: 

177 user = FederatedUser.get_from_federated_username( 

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

179 ) 

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

181 except FederatedUser.DoesNotExist: 

182 auth = None 

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

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

185 # current querystring 

186 if settings.CAS_FEDERATE: 

187 if auth is not None: 

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

189 url = auth.get_logout_url() 

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

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

192 response.delete_cookie("remember_provider") 

193 return self.delete_cookies(response) 

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

195 if self.service: 

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

197 return self.delete_cookies(HttpResponseRedirect(self.service)) 

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

199 elif self.url: 

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

201 return self.delete_cookies(HttpResponseRedirect(self.url)) 

202 else: 

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

204 if session_nb == 1: 

205 logout_msg = mark_safe(_( 

206 "<h3>Logout successful</h3>" 

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

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

209 )) 

210 elif session_nb > 1: 

211 logout_msg = mark_safe(_( 

212 "<h3>Logout successful</h3>" 

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

214 "Authentication Service. " 

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

216 ) % session_nb) 

217 else: 

218 logout_msg = mark_safe(_( 

219 "<h3>Logout successful</h3>" 

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

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

222 )) 

223 

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

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

226 if settings.CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT: 

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

228 if self.ajax: 

229 url = reverse("cas_server:login") 

230 data = { 

231 'status': 'success', 

232 'detail': 'logout', 

233 'url': url, 

234 'session_nb': session_nb 

235 } 

236 return self.delete_cookies(json_response(request, data)) 

237 else: 

238 return self.delete_cookies(redirect("cas_server:login")) 

239 else: 

240 if self.ajax: 

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

242 return self.delete_cookies(json_response(request, data)) 

243 else: 

244 return self.delete_cookies(render( 

245 request, 

246 settings.CAS_LOGOUT_TEMPLATE, 

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

248 )) 

249 

250 

251class FederateAuth(CsrfExemptView): 

252 """ 

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

254 

255 csrf is disabled for allowing SLO requests reception. 

256 """ 

257 

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

259 service_url = None 

260 

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

262 """ 

263 return a CAS client object matching provider 

264 

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

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

267 :return: The user CAS client object 

268 :rtype: :class:`federate.CASFederateValidateUser 

269 <cas_server.federate.CASFederateValidateUser>` 

270 """ 

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

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

273 self.service_url = service_url 

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

275 

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

277 """ 

278 method called on POST request 

279 

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

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

282 """ 

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

284 if not settings.CAS_FEDERATE: 

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

286 return redirect("cas_server:login") 

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

288 # allowing SLO requests reception 

289 try: 

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

291 auth = self.get_cas_client(request, provider) 

292 try: 

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

294 except (KeyError, AttributeError): 

295 pass 

296 return HttpResponse("ok") 

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

298 except FederatedIendityProvider.DoesNotExist: 

299 # Manually checking for csrf to protect the code below 

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

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

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

303 return reason # Failed the test, stop here. 

304 form = import_attr(settings.CAS_FEDERATE_SELECT_FORM)(request.POST) 

305 if form.is_valid(): 

306 params = utils.copy_params( 

307 request.POST, 

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

309 ) 

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

311 del params["renew"] 

312 url = utils.reverse_params( 

313 "cas_server:federateAuth", 

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

315 params=params 

316 ) 

317 return HttpResponseRedirect(url) 

318 else: 

319 return redirect("cas_server:login") 

320 

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

322 """ 

323 method called on GET request 

324 

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

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

327 """ 

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

329 if not settings.CAS_FEDERATE: 

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

331 return redirect("cas_server:login") 

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

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

334 # identity provider. 

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

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

337 return redirect("cas_server:login") 

338 try: 

339 # get the identity provider from its suffix 

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

341 # get a CAS client for the user identity provider 

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

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

344 if 'ticket' not in request.GET: 

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

346 return HttpResponseRedirect(auth.get_login_url()) 

347 else: 

348 ticket = request.GET['ticket'] 

349 try: 

350 # if the ticket validation succeed 

351 if auth.verify_ticket(ticket): 

352 logger.info( 

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

354 auth.username, 

355 auth.provider.server_url 

356 ) 

357 ) 

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

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

360 request.session["federate_ticket"] = ticket 

361 auth.register_slo( 

362 auth.federated_username, 

363 request.session.session_key, 

364 ticket 

365 ) 

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

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

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

369 response = HttpResponseRedirect(url) 

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

371 # cookie 

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

373 max_age = settings.CAS_FEDERATE_REMEMBER_TIMEOUT 

374 utils.set_cookie( 

375 response, 

376 "remember_provider", 

377 provider.suffix, 

378 max_age 

379 ) 

380 return response 

381 # else redirect to the identity provider CAS login page 

382 else: 

383 logger.info( 

384 ( 

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

386 "Retrying authentication" 

387 ) % ( 

388 ticket, 

389 auth.provider.server_url, 

390 self.service_url 

391 ) 

392 ) 

393 return HttpResponseRedirect(auth.get_login_url()) 

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

395 except SyntaxError as error: 

396 messages.add_message( 

397 request, 

398 messages.ERROR, 

399 _( 

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

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

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

403 ) 

404 response = redirect("cas_server:login") 

405 response.delete_cookie("remember_provider") 

406 return response 

407 except FederatedIendityProvider.DoesNotExist: 

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

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

410 return redirect("cas_server:login") 

411 

412 

413class LoginView(View, LogoutMixin): 

414 """credential requestor / acceptor""" 

415 

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

417 # Nine is reasonable in this case. 

418 

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

420 user = None 

421 #: The form to display to the user 

422 form = None 

423 

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

425 request = None 

426 #: service GET/POST parameter 

427 service = None 

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

429 renew = None 

430 #: the warn GET/POST parameter 

431 warn = None 

432 #: the gateway GET/POST parameter 

433 gateway = None 

434 #: the method GET/POST parameter 

435 method = None 

436 

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

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

439 ajax = None 

440 

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

442 renewed = False 

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

444 warned = False 

445 

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

447 #: is ``True``) 

448 username = None 

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

450 #: ``True``) 

451 ticket = None 

452 

453 INVALID_LOGIN_TICKET = 1 

454 USER_LOGIN_OK = 2 

455 USER_LOGIN_FAILURE = 3 

456 USER_ALREADY_LOGGED = 4 

457 USER_AUTHENTICATED = 5 

458 USER_NOT_AUTHENTICATED = 6 

459 

460 def init_post(self, request): 

461 """ 

462 Initialize POST received parameters 

463 

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

465 """ 

466 self.request = request 

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

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

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

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

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

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

473 self.warned = True 

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

475 if settings.CAS_FEDERATE: 

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

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

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

479 

480 def gen_lt(self): 

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

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

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

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

485 

486 def check_lt(self): 

487 """ 

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

489 

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

491 :rtype: bool 

492 """ 

493 # save LT for later check 

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

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

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

497 self.gen_lt() 

498 # check if send LT is valid 

499 if lt_send not in lt_valid: 

500 return False 

501 else: 

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

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

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

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

506 return True 

507 

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

509 """ 

510 method called on POST request on this view 

511 

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

513 """ 

514 # initialize class parameters 

515 self.init_post(request) 

516 # process the POST request 

517 ret = self.process_post() 

518 if ret == self.INVALID_LOGIN_TICKET: 

519 messages.add_message( 

520 self.request, 

521 messages.ERROR, 

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

523 ) 

524 elif ret == self.USER_LOGIN_OK: 

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

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

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

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

529 session_key=self.request.session.session_key 

530 )[0] 

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

532 self.user.save() 

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

534 if settings.CAS_FEDERATE: 

535 self.ticket = None 

536 self.username = None 

537 self.init_form() 

538 # preserve valid LoginTickets from session flush 

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

540 # On login failure, flush the session 

541 self.logout() 

542 # restore valid LoginTickets 

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

544 elif ret == self.USER_ALREADY_LOGGED: 

545 pass 

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

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

548 # call the GET/POST common part 

549 response = self.common() 

550 if self.warn: 

551 utils.set_cookie( 

552 response, 

553 "warn", 

554 "on", 

555 10 * 365 * 24 * 3600 

556 ) 

557 else: 

558 response.delete_cookie("warn") 

559 return response 

560 

561 def process_post(self): 

562 """ 

563 Analyse the POST request: 

564 

565 * check that the LoginTicket is valid 

566 * check that the user sumited credentials are valid 

567 

568 :return: 

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

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

571 reauthentication. 

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

573 reauthentication and his credentials are not valid 

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

575 reauthentication and his credentials are valid 

576 :rtype: int 

577 """ 

578 if not self.check_lt(): 

579 self.init_form(self.request.POST) 

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

581 return self.INVALID_LOGIN_TICKET 

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

583 # authentication request receive, initialize the form to use 

584 self.init_form(self.request.POST) 

585 if self.form.is_valid(): 

586 self.request.session.set_expiry(0) 

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

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

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

590 self.renewed = True 

591 self.warned = True 

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

593 return self.USER_LOGIN_OK 

594 else: 

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

596 return self.USER_LOGIN_FAILURE 

597 else: 

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

599 return self.USER_ALREADY_LOGGED 

600 

601 def init_get(self, request): 

602 """ 

603 Initialize GET received parameters 

604 

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

606 """ 

607 self.request = request 

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

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

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

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

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

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

614 if settings.CAS_FEDERATE: 

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

616 # FederateAuth.get 

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

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

619 if self.username: 

620 del request.session["federate_username"] 

621 if self.ticket: 

622 del request.session["federate_ticket"] 

623 

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

625 """ 

626 method called on GET request on this view 

627 

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

629 """ 

630 # initialize class parameters 

631 self.init_get(request) 

632 # process the GET request 

633 self.process_get() 

634 # call the GET/POST common part 

635 return self.common() 

636 

637 def process_get(self): 

638 """ 

639 Analyse the GET request 

640 

641 :return: 

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

643 for authentication renewal 

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

645 for authentication renewal 

646 :rtype: int 

647 """ 

648 # generate a new LT 

649 self.gen_lt() 

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

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

652 self.init_form() 

653 return self.USER_NOT_AUTHENTICATED 

654 return self.USER_AUTHENTICATED 

655 

656 def init_form(self, values=None): 

657 """ 

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

659 

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

661 """ 

662 if values: 

663 values = values.copy() 

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

665 form_initial = { 

666 'service': self.service, 

667 'method': self.method, 

668 'warn': ( 

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

670 ), 

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

672 'renew': self.renew 

673 } 

674 if settings.CAS_FEDERATE: 

675 if self.username and self.ticket: 

676 form_initial['username'] = self.username 

677 form_initial['password'] = self.ticket 

678 form_initial['ticket'] = self.ticket 

679 self.form = import_attr(settings.CAS_FEDERATE_USER_CREDENTIAL_FORM)( 

680 values, 

681 initial=form_initial 

682 ) 

683 else: 

684 self.form = import_attr( 

685 settings.CAS_FEDERATE_SELECT_FORM 

686 )(values, initial=form_initial) 

687 else: 

688 self.form = import_attr(settings.CAS_USER_CREDENTIAL_FORM)( 

689 values, 

690 initial=form_initial 

691 ) 

692 

693 def service_login(self): 

694 """ 

695 Perform login against a service 

696 

697 :return: 

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

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

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

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

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

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

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

705 :rtype: django.http.HttpResponse 

706 """ 

707 try: 

708 # is the service allowed 

709 service_pattern = ServicePattern.validate(self.service) 

710 # is the current user allowed on this service 

711 service_pattern.check_user(self.user) 

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

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

714 messages.add_message( 

715 self.request, 

716 messages.WARNING, 

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

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

719 ) 

720 if self.ajax: 

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

722 return json_response(self.request, data) 

723 else: 

724 warn_form = import_attr(settings.CAS_WARN_FORM)(initial={ 

725 'service': self.service, 

726 'renew': self.renew, 

727 'gateway': self.gateway, 

728 'method': self.method, 

729 'warned': True, 

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

731 }) 

732 return render( 

733 self.request, 

734 settings.CAS_WARN_TEMPLATE, 

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

736 ) 

737 else: 

738 # redirect, using method ? 

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

740 redirect_url = self.user.get_service_url( 

741 self.service, 

742 service_pattern, 

743 renew=self.renewed 

744 ) 

745 if not self.ajax: 

746 return HttpResponseRedirect(redirect_url) 

747 else: 

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

749 return json_response(self.request, data) 

750 except ServicePattern.DoesNotExist: 

751 error = 1 

752 messages.add_message( 

753 self.request, 

754 messages.ERROR, 

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

756 ) 

757 except models.BadUsername: 

758 error = 2 

759 messages.add_message( 

760 self.request, 

761 messages.ERROR, 

762 _(u"Username not allowed") 

763 ) 

764 except models.BadFilter: 

765 error = 3 

766 messages.add_message( 

767 self.request, 

768 messages.ERROR, 

769 _(u"User characteristics not allowed") 

770 ) 

771 except models.UserFieldNotDefined: 

772 error = 4 

773 messages.add_message( 

774 self.request, 

775 messages.ERROR, 

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

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

778 ) 

779 

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

781 if self.gateway and not self.ajax: 

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

783 return HttpResponseRedirect(self.service) 

784 

785 if not self.ajax: 

786 return render( 

787 self.request, 

788 settings.CAS_LOGGED_TEMPLATE, 

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

790 ) 

791 else: 

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

793 return json_response(self.request, data) 

794 

795 def authenticated(self): 

796 """ 

797 Processing authenticated users 

798 

799 :return: 

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

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

802 :rtype: django.http.HttpResponse 

803 """ 

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

805 # session 

806 try: 

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

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

809 session_key=self.request.session.session_key 

810 ) 

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

812 except models.User.DoesNotExist: 

813 logger.warning( 

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

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

816 ) 

817 ) 

818 self.logout() 

819 if self.ajax: 

820 data = { 

821 "status": "error", 

822 "detail": "login required", 

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

824 } 

825 return json_response(self.request, data) 

826 else: 

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

828 

829 # if login against a service 

830 if self.service: 

831 return self.service_login() 

832 # else display the logged template 

833 else: 

834 if self.ajax: 

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

836 return json_response(self.request, data) 

837 else: 

838 return render( 

839 self.request, 

840 settings.CAS_LOGGED_TEMPLATE, 

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

842 ) 

843 

844 def not_authenticated(self): 

845 """ 

846 Processing non authenticated users 

847 

848 :return: 

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

850 depending of GET/POST parameters 

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

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

853 :rtype: django.http.HttpResponse 

854 """ 

855 if self.service: 

856 try: 

857 service_pattern = ServicePattern.validate(self.service) 

858 if self.gateway and not self.ajax: 

859 # clean messages before leaving django 

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

861 return HttpResponseRedirect(self.service) 

862 

863 if settings.CAS_SHOW_SERVICE_MESSAGES: 

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

865 messages.add_message( 

866 self.request, 

867 messages.WARNING, 

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

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

870 ) 

871 else: 

872 messages.add_message( 

873 self.request, 

874 messages.WARNING, 

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

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

877 ) 

878 except ServicePattern.DoesNotExist: 

879 if settings.CAS_SHOW_SERVICE_MESSAGES: 

880 messages.add_message( 

881 self.request, 

882 messages.ERROR, 

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

884 ) 

885 if self.ajax: 

886 data = { 

887 "status": "error", 

888 "detail": "login required", 

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

890 } 

891 return json_response(self.request, data) 

892 else: 

893 if settings.CAS_FEDERATE: 

894 if self.username and self.ticket: 

895 return render( 

896 self.request, 

897 settings.CAS_LOGIN_TEMPLATE, 

898 utils.context({ 

899 'form': self.form, 

900 'auto_submit': True, 

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

902 }) 

903 ) 

904 else: 

905 if ( 

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

907 FederatedIendityProvider.objects.filter( 

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

909 ) 

910 ): 

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

912 url = utils.reverse_params( 

913 "cas_server:federateAuth", 

914 params=params, 

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

916 ) 

917 return HttpResponseRedirect(url) 

918 else: 

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

920 # to the user identity provider 

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

922 try: 

923 user = FederatedUser.get_from_federated_username( 

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

925 ) 

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

927 url = utils.reverse_params( 

928 "cas_server:federateAuth", 

929 params=params, 

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

931 ) 

932 return HttpResponseRedirect(url) 

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

934 # database. 

935 except FederatedUser.DoesNotExist: # pragma: no cover 

936 pass 

937 return render( 

938 self.request, 

939 settings.CAS_LOGIN_TEMPLATE, 

940 utils.context({ 

941 'form': self.form, 

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

943 }) 

944 ) 

945 else: 

946 return render( 

947 self.request, 

948 settings.CAS_LOGIN_TEMPLATE, 

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

950 ) 

951 

952 def common(self): 

953 """ 

954 Common part execute uppon GET and POST request 

955 

956 :return: 

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

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

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

960 :rtype: django.http.HttpResponse 

961 """ 

962 # if authenticated and successfully renewed authentication if needed 

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

964 return self.authenticated() 

965 else: 

966 return self.not_authenticated() 

967 

968 

969class Auth(CsrfExemptView): 

970 """ 

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

972 

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

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

975 """ 

976 

977 @staticmethod 

978 def post(request): 

979 """ 

980 method called on POST request on this view 

981 

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

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

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

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

986 line. 

987 :rtype: django.http.HttpResponse 

988 """ 

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

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

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

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

993 

994 if not settings.CAS_AUTH_SHARED_SECRET: 

995 return HttpResponse( 

996 "no\nplease set CAS_AUTH_SHARED_SECRET", 

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

998 ) 

999 if secret != settings.CAS_AUTH_SHARED_SECRET: 

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

1001 if not username or not password or not service: 

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

1003 form = import_attr(settings.CAS_USER_CREDENTIAL_FORM)( 

1004 request.POST, 

1005 initial={ 

1006 'service': service, 

1007 'method': 'POST', 

1008 'warn': False 

1009 } 

1010 ) 

1011 if form.is_valid(): 

1012 try: 

1013 user = models.User.objects.get_or_create( 

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

1015 session_key=request.session.session_key 

1016 )[0] 

1017 user.save() 

1018 # is the service allowed 

1019 service_pattern = ServicePattern.validate(service) 

1020 # is the current user allowed on this service 

1021 service_pattern.check_user(user) 

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

1023 user.delete() 

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

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

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

1027 else: 

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

1029 

1030 

1031class Validate(View): 

1032 """service ticket validation""" 

1033 @staticmethod 

1034 def get(request): 

1035 """ 

1036 method called on GET request on this view 

1037 

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

1039 :return: 

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

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

1042 :rtype: django.http.HttpResponse 

1043 """ 

1044 # store wanted GET parameters 

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

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

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

1048 # service and ticket parameters are mandatory 

1049 if service and ticket: 

1050 try: 

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

1052 # still valid 

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

1054 logger.info( 

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

1056 ticket.value, 

1057 ticket.user.username, 

1058 ticket.service 

1059 ) 

1060 ) 

1061 return HttpResponse( 

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

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

1064 ) 

1065 except ServiceTicket.DoesNotExist: 

1066 logger.warning( 

1067 ( 

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

1069 "already validated, auth to %s failed" 

1070 ) % ( 

1071 ticket, 

1072 service 

1073 ) 

1074 ) 

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

1076 else: 

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

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

1079 

1080 

1081@python_2_unicode_compatible 

1082class ValidationBaseError(Exception): 

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

1084 

1085 #: The error code 

1086 code = None 

1087 #: The error message 

1088 msg = None 

1089 

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

1091 self.code = code 

1092 self.msg = msg 

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

1094 

1095 def __str__(self): 

1096 return u"%s" % self.msg 

1097 

1098 def render(self, request): 

1099 """ 

1100 render the error template for the exception 

1101 

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

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

1104 :rtype: django.http.HttpResponse 

1105 """ 

1106 return render( 

1107 request, 

1108 self.template, 

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

1110 ) 

1111 

1112 

1113class ValidateError(ValidationBaseError): 

1114 """handle service validation error""" 

1115 

1116 #: template to be render for the error 

1117 template = "cas_server/serviceValidateError.xml" 

1118 

1119 def context(self): 

1120 """ 

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

1122 

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

1124 :rtype: dict 

1125 """ 

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

1127 

1128 

1129class ValidateService(View): 

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

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

1132 request = None 

1133 #: The service GET parameter 

1134 service = None 

1135 #: the ticket GET parameter 

1136 ticket = None 

1137 #: the pgtUrl GET parameter 

1138 pgt_url = None 

1139 #: the renew GET parameter 

1140 renew = None 

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

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

1143 allow_proxy_ticket = False 

1144 

1145 def get(self, request): 

1146 """ 

1147 method called on GET request on this view 

1148 

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

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

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

1152 :rtype: django.http.HttpResponse 

1153 """ 

1154 # define the class parameters 

1155 self.request = request 

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

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

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

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

1160 

1161 # service and ticket parameter are mandatory 

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

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

1164 return ValidateError( 

1165 u'INVALID_REQUEST', 

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

1167 ).render(request) 

1168 else: 

1169 try: 

1170 # search the ticket in the database 

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

1172 # prepare template rendering context 

1173 params = { 

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

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

1176 'proxies': proxies, 

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

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

1179 } 

1180 # if pgtUrl is set, require https or localhost 

1181 if self.pgt_url and ( 

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

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

1184 ): 

1185 return self.process_pgturl(params) 

1186 else: 

1187 logger.info( 

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

1189 self.ticket.value, 

1190 self.ticket.user.username, 

1191 self.ticket.service 

1192 ) 

1193 ) 

1194 logger.debug( 

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

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

1197 ) 

1198 ) 

1199 return render( 

1200 request, 

1201 "cas_server/serviceValidate.xml", 

1202 params, 

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

1204 ) 

1205 except ValidateError as error: 

1206 logger.warning( 

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

1208 ) 

1209 return error.render(request) 

1210 

1211 def process_ticket(self): 

1212 """ 

1213 fetch the ticket against the database and check its validity 

1214 

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

1216 service 

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

1218 :rtype: :obj:`tuple` 

1219 """ 

1220 try: 

1221 proxies = [] 

1222 if self.allow_proxy_ticket: 

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

1224 else: 

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

1226 try: 

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

1228 proxies.append(prox.url) 

1229 except AttributeError: 

1230 pass 

1231 if ticket.service != self.service: 

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

1233 return ticket, proxies 

1234 except Ticket.DoesNotExist: 

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

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

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

1238 

1239 def process_pgturl(self, params): 

1240 """ 

1241 Handle PGT request 

1242 

1243 :param dict params: A template context dict 

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

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

1246 :rtype: django.http.HttpResponse 

1247 """ 

1248 try: 

1249 pattern = ServicePattern.validate(self.pgt_url) 

1250 if pattern.proxy_callback: 

1251 proxyid = utils.gen_pgtiou() 

1252 pticket = ProxyGrantingTicket.objects.create( 

1253 user=self.ticket.user, 

1254 service=self.pgt_url, 

1255 service_pattern=pattern, 

1256 single_log_out=pattern.single_log_out 

1257 ) 

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

1259 try: 

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

1261 if ret.status_code == 200: 

1262 params['proxyGrantingTicket'] = proxyid 

1263 else: 

1264 pticket.delete() 

1265 logger.info( 

1266 ( 

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

1268 "Proxy Granting Ticket transmited to %s." 

1269 ) % ( 

1270 self.ticket.value, 

1271 self.ticket.user.username, 

1272 self.ticket.service, 

1273 self.pgt_url 

1274 ) 

1275 ) 

1276 logger.debug( 

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

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

1279 ) 

1280 ) 

1281 return render( 

1282 self.request, 

1283 "cas_server/serviceValidate.xml", 

1284 params, 

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

1286 ) 

1287 except requests.exceptions.RequestException as error: 

1288 error = utils.unpack_nested_exception(error) 

1289 raise ValidateError( 

1290 u'INVALID_PROXY_CALLBACK', 

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

1292 ) 

1293 else: 

1294 raise ValidateError( 

1295 u'INVALID_PROXY_CALLBACK', 

1296 u"callback url not allowed by configuration" 

1297 ) 

1298 except ServicePattern.DoesNotExist: 

1299 raise ValidateError( 

1300 u'INVALID_PROXY_CALLBACK', 

1301 u'callback url not allowed by configuration' 

1302 ) 

1303 

1304 

1305class Proxy(View): 

1306 """proxy ticket service""" 

1307 

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

1309 request = None 

1310 #: A ProxyGrantingTicket from the pgt GET parameter 

1311 pgt = None 

1312 #: the targetService GET parameter 

1313 target_service = None 

1314 

1315 def get(self, request): 

1316 """ 

1317 method called on GET request on this view 

1318 

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

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

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

1322 :rtype: django.http.HttpResponse 

1323 """ 

1324 self.request = request 

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

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

1327 try: 

1328 # pgt and targetService parameters are mandatory 

1329 if self.pgt and self.target_service: 

1330 return self.process_proxy() 

1331 else: 

1332 raise ValidateError( 

1333 u'INVALID_REQUEST', 

1334 u"you must specify and pgt and targetService" 

1335 ) 

1336 except ValidateError as error: 

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

1338 return error.render(request) 

1339 

1340 def process_proxy(self): 

1341 """ 

1342 handle PT request 

1343 

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

1345 the user not allowed on the tardet service. 

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

1347 :rtype: django.http.HttpResponse 

1348 """ 

1349 try: 

1350 # is the target service allowed 

1351 pattern = ServicePattern.validate(self.target_service) 

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

1353 if not pattern.proxy: 

1354 raise ValidateError( 

1355 u'UNAUTHORIZED_SERVICE', 

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

1357 ) 

1358 # is the proxy granting ticket valid 

1359 ticket = ProxyGrantingTicket.get(self.pgt) 

1360 # is the pgt user allowed on the target service 

1361 pattern.check_user(ticket.user) 

1362 pticket = ticket.user.get_ticket( 

1363 ProxyTicket, 

1364 self.target_service, 

1365 pattern, 

1366 renew=False 

1367 ) 

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

1369 logger.info( 

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

1371 ticket.user.username, 

1372 self.target_service 

1373 ) 

1374 ) 

1375 return render( 

1376 self.request, 

1377 "cas_server/proxy.xml", 

1378 {'ticket': pticket.value}, 

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

1380 ) 

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

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

1383 except ServicePattern.DoesNotExist: 

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

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

1386 raise ValidateError( 

1387 u'UNAUTHORIZED_USER', 

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

1389 ) 

1390 

1391 

1392class SamlValidateError(ValidationBaseError): 

1393 """handle saml validation error""" 

1394 

1395 #: template to be render for the error 

1396 template = "cas_server/samlValidateError.xml" 

1397 

1398 def context(self): 

1399 """ 

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

1401 :rtype: dict 

1402 """ 

1403 return { 

1404 'code': self.code, 

1405 'msg': self.msg, 

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

1407 'ResponseID': utils.gen_saml_id() 

1408 } 

1409 

1410 

1411class SamlValidate(CsrfExemptView): 

1412 """SAML ticket validation""" 

1413 request = None 

1414 target = None 

1415 ticket = None 

1416 root = None 

1417 

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

1419 """ 

1420 method called on POST request on this view 

1421 

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

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

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

1425 :rtype: django.http.HttpResponse 

1426 """ 

1427 self.request = request 

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

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

1430 try: 

1431 self.ticket = self.process_ticket() 

1432 expire_instant = (self.ticket.creation + 

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

1434 params = { 

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

1436 'expireInstant': expire_instant, 

1437 'Recipient': self.target, 

1438 'ResponseID': utils.gen_saml_id(), 

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

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

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

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

1443 

1444 } 

1445 logger.info( 

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

1447 self.ticket.value, 

1448 self.ticket.user.username, 

1449 self.ticket.service 

1450 ) 

1451 ) 

1452 logger.debug( 

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

1454 ) 

1455 

1456 return render( 

1457 request, 

1458 "cas_server/samlValidate.xml", 

1459 params, 

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

1461 ) 

1462 except SamlValidateError as error: 

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

1464 return error.render(request) 

1465 

1466 def process_ticket(self): 

1467 """ 

1468 validate ticket from SAML XML body 

1469 

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

1471 to parse the posted XML. 

1472 :return: a ticket object 

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

1474 """ 

1475 try: 

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

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

1478 ticket = models.Ticket.get(ticket) 

1479 if ticket.service != self.target: 

1480 raise SamlValidateError( 

1481 u'AuthnFailed', 

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

1483 ) 

1484 return ticket 

1485 except (IndexError, KeyError): 

1486 raise SamlValidateError(u'VersionMismatch') 

1487 except Ticket.DoesNotExist: 

1488 raise SamlValidateError( 

1489 u'AuthnFailed', 

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

1491 ) 

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

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