views.py 57.7 KB
Newer Older
Valentin Samir's avatar
Valentin Samir committed
1
# -*- coding: utf-8 -*-
Valentin Samir's avatar
Valentin Samir committed
2 3 4 5 6 7 8 9 10
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License version 3 for
# more details.
#
# You should have received a copy of the GNU General Public License version 3
# along with this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
Valentin Samir's avatar
Valentin Samir committed
11
# (c) 2015-2016 Valentin Samir
Valentin Samir's avatar
Valentin Samir committed
12
"""views for the app"""
13
from .default_settings import settings, SessionStore
14

Valentin Samir's avatar
Valentin Samir committed
15
from django.shortcuts import render, redirect
16
from django.core.urlresolvers import reverse
Valentin Samir's avatar
Valentin Samir committed
17
from django.http import HttpResponse, HttpResponseRedirect
Valentin Samir's avatar
Valentin Samir committed
18
from django.contrib import messages
19
from django.utils.decorators import method_decorator
Valentin Samir's avatar
Valentin Samir committed
20
from django.utils.translation import ugettext as _
Valentin Samir's avatar
Valentin Samir committed
21
from django.utils import timezone
22
from django.views.decorators.csrf import csrf_exempt
23
from django.middleware.csrf import CsrfViewMiddleware
24
from django.views.generic import View
25
from django.utils.encoding import python_2_unicode_compatible
Valentin Samir's avatar
Valentin Samir committed
26

27
import re
Valentin Samir's avatar
Valentin Samir committed
28 29
import logging
import pprint
Valentin Samir's avatar
Valentin Samir committed
30
import requests
Valentin Samir's avatar
Valentin Samir committed
31
from lxml import etree
Valentin Samir's avatar
Valentin Samir committed
32
from datetime import timedelta
Valentin Samir's avatar
Valentin Samir committed
33

Valentin Samir's avatar
Valentin Samir committed
34 35 36
import cas_server.utils as utils
import cas_server.forms as forms
import cas_server.models as models
Valentin Samir's avatar
Valentin Samir committed
37

Valentin Samir's avatar
style  
Valentin Samir committed
38
from .utils import json_response
Valentin Samir's avatar
Valentin Samir committed
39
from .models import Ticket, ServiceTicket, ProxyTicket, ProxyGrantingTicket
40
from .models import ServicePattern, FederatedIendityProvider, FederatedUser
Valentin Samir's avatar
Valentin Samir committed
41
from .federate import CASFederateValidateUser
Valentin Samir's avatar
Valentin Samir committed
42

Valentin Samir's avatar
Valentin Samir committed
43 44
logger = logging.getLogger(__name__)

Valentin Samir's avatar
PEP8  
Valentin Samir committed
45

Valentin Samir's avatar
Valentin Samir committed
46
class LogoutMixin(object):
47
    """destroy CAS session utils"""
Valentin Samir's avatar
style  
Valentin Samir committed
48
    def logout(self, all_session=False):
Valentin Samir's avatar
Valentin Samir committed
49 50 51 52 53 54 55 56 57
        """
            effectively destroy a CAS session

            :param boolean all_session: If ``True`` destroy all the user sessions, otherwise
                destroy the current user session.
            :return: The number of destroyed sessions
            :rtype: int
        """
        # initialize the counter of the number of destroyed sesisons
58
        session_nb = 0
Valentin Samir's avatar
Valentin Samir committed
59
        # save the current user username before flushing the session
Valentin Samir's avatar
Valentin Samir committed
60 61
        username = self.request.session.get("username")
        if username:
Valentin Samir's avatar
style  
Valentin Samir committed
62
            if all_session:
Valentin Samir's avatar
Valentin Samir committed
63 64 65
                logger.info("Logging out user %s from all of they sessions." % username)
            else:
                logger.info("Logging out user %s." % username)
66
        # logout the user from the current session
67
        try:
Valentin Samir's avatar
Valentin Samir committed
68
            user = models.User.objects.get(
69
                username=username,
Valentin Samir's avatar
oops  
Valentin Samir committed
70
                session_key=self.request.session.session_key
Valentin Samir's avatar
Valentin Samir committed
71
            )
Valentin Samir's avatar
Valentin Samir committed
72
            # flush the session
73
            self.request.session.flush()
Valentin Samir's avatar
Valentin Samir committed
74
            # send SLO requests
75
            user.logout(self.request)
Valentin Samir's avatar
Valentin Samir committed
76
            # delete the user
77
            user.delete()
Valentin Samir's avatar
Valentin Samir committed
78
            # increment the destroyed session counter
79
            session_nb += 1
80
        except models.User.DoesNotExist:
81
            # if user not found in database, flush the session anyway
82
            self.request.session.flush()
83

Valentin Samir's avatar
style  
Valentin Samir committed
84 85
        # If all_session is set logout user from alternative sessions
        if all_session:
Valentin Samir's avatar
Valentin Samir committed
86
            # Iterate over all user sessions
87
            for user in models.User.objects.filter(username=username):
Valentin Samir's avatar
Valentin Samir committed
88
                # get the user session
89
                session = SessionStore(session_key=user.session_key)
Valentin Samir's avatar
Valentin Samir committed
90
                # flush the session
91
                session.flush()
Valentin Samir's avatar
Valentin Samir committed
92
                # send SLO requests
93
                user.logout(self.request)
Valentin Samir's avatar
Valentin Samir committed
94
                # delete the user
95
                user.delete()
Valentin Samir's avatar
Valentin Samir committed
96
                # increment the destroyed session counter
97
                session_nb += 1
Valentin Samir's avatar
Valentin Samir committed
98 99
        if username:
            logger.info("User %s logged out" % username)
100
        return session_nb
101

Valentin Samir's avatar
PEP8  
Valentin Samir committed
102

Valentin Samir's avatar
Valentin Samir committed
103 104 105
class LogoutView(View, LogoutMixin):
    """destroy CAS session (logout) view"""

Valentin Samir's avatar
Valentin Samir committed
106
    #: current :class:`django.http.HttpRequest` object
Valentin Samir's avatar
Valentin Samir committed
107
    request = None
Valentin Samir's avatar
Valentin Samir committed
108
    #: service GET parameter
Valentin Samir's avatar
Valentin Samir committed
109
    service = None
Valentin Samir's avatar
Valentin Samir committed
110 111 112 113 114
    #: url GET paramet
    url = None
    #: ``True`` if the HTTP_X_AJAX http header is sent and ``settings.CAS_ENABLE_AJAX_AUTH``
    #: is ``True``, ``False`` otherwise.
    ajax = None
Valentin Samir's avatar
Valentin Samir committed
115

Valentin Samir's avatar
Valentin Samir committed
116
    def init_get(self, request):
Valentin Samir's avatar
Valentin Samir committed
117 118 119 120 121
        """
            Initialize the :class:`LogoutView` attributes on GET request

            :param django.http.HttpRequest request: The current request object
        """
122 123
        self.request = request
        self.service = request.GET.get('service')
Valentin Samir's avatar
Valentin Samir committed
124
        self.url = request.GET.get('url')
125
        self.ajax = settings.CAS_ENABLE_AJAX_AUTH and 'HTTP_X_AJAX' in request.META
Valentin Samir's avatar
Valentin Samir committed
126 127

    def get(self, request, *args, **kwargs):
Valentin Samir's avatar
Valentin Samir committed
128 129 130 131 132
        """
            methode called on GET request on this view

            :param django.http.HttpRequest request: The current request object
        """
Valentin Samir's avatar
Valentin Samir committed
133
        logger.info("logout requested")
Valentin Samir's avatar
Valentin Samir committed
134
        # initialize the class attributes
Valentin Samir's avatar
Valentin Samir committed
135
        self.init_get(request)
Valentin Samir's avatar
Valentin Samir committed
136 137
        # if CAS federation mode is enable, bakup the provider before flushing the sessions
        if settings.CAS_FEDERATE:
138 139 140 141 142 143
            try:
                user = FederatedUser.get_from_federated_username(
                    self.request.session.get("username")
                )
                auth = CASFederateValidateUser(user.provider, service_url="")
            except FederatedUser.DoesNotExist:
144
                auth = None
145
        session_nb = self.logout(self.request.GET.get("all"))
Valentin Samir's avatar
Valentin Samir committed
146 147
        # if CAS federation mode is enable, redirect to user CAS logout page, appending the
        # current querystring
Valentin Samir's avatar
Valentin Samir committed
148
        if settings.CAS_FEDERATE:
149 150
            if auth is not None:
                params = utils.copy_params(request.GET)
151
                url = auth.get_logout_url()
152
                return HttpResponseRedirect(utils.update_url(url, params))
153 154
        # if service is set, redirect to service after logout
        if self.service:
Valentin Samir's avatar
PEP8  
Valentin Samir committed
155
            list(messages.get_messages(request))  # clean messages before leaving the django app
156
            return HttpResponseRedirect(self.service)
Valentin Samir's avatar
Valentin Samir committed
157
        # if service is not set but url is set, redirect to url after logout
Valentin Samir's avatar
Valentin Samir committed
158
        elif self.url:
Valentin Samir's avatar
PEP8  
Valentin Samir committed
159
            list(messages.get_messages(request))  # clean messages before leaving the django app
Valentin Samir's avatar
Valentin Samir committed
160
            return HttpResponseRedirect(self.url)
161
        else:
Valentin Samir's avatar
Valentin Samir committed
162
            # build logout message depending of the number of sessions the user logs out
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
            if session_nb == 1:
                logout_msg = _(
                    "<h3>Logout successful</h3>"
                    "You have successfully logged out from the Central Authentication Service. "
                    "For security reasons, exit your web browser."
                )
            elif session_nb > 1:
                logout_msg = _(
                    "<h3>Logout successful</h3>"
                    "You have successfully logged out from %s sessions of the Central "
                    "Authentication Service. "
                    "For security reasons, exit your web browser."
                ) % session_nb
            else:
                logout_msg = _(
                    "<h3>Logout successful</h3>"
                    "You were already logged out from the Central Authentication Service. "
                    "For security reasons, exit your web browser."
                )

Valentin Samir's avatar
Valentin Samir committed
183 184
            # depending of settings, redirect to the login page with a logout message or display
            # the logout page. The default is to display tge logout page.
185
            if settings.CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT:
186
                messages.add_message(request, messages.SUCCESS, logout_msg)
187 188
                if self.ajax:
                    url = reverse("cas_server:login")
189 190 191 192 193 194
                    data = {
                        'status': 'success',
                        'detail': 'logout',
                        'url': url,
                        'session_nb': session_nb
                    }
Valentin Samir's avatar
style  
Valentin Samir committed
195
                    return json_response(request, data)
196 197
                else:
                    return redirect("cas_server:login")
198
            else:
199
                if self.ajax:
200
                    data = {'status': 'success', 'detail': 'logout', 'session_nb': session_nb}
Valentin Samir's avatar
style  
Valentin Samir committed
201
                    return json_response(request, data)
202
                else:
203 204 205
                    return render(
                        request,
                        settings.CAS_LOGOUT_TEMPLATE,
206
                        utils.context({'logout_msg': logout_msg})
207
                    )
208

Valentin Samir's avatar
PEP8  
Valentin Samir committed
209

Valentin Samir's avatar
Valentin Samir committed
210
class FederateAuth(View):
Valentin Samir's avatar
Valentin Samir committed
211
    """view to authenticated user agains a backend CAS then CAS_FEDERATE is True"""
Valentin Samir's avatar
Valentin Samir committed
212
    @method_decorator(csrf_exempt)  # csrf is disabled for allowing SLO requests reception
213
    def dispatch(self, request, *args, **kwargs):
Valentin Samir's avatar
Valentin Samir committed
214 215 216 217 218
        """
            dispatch different http request to the methods of the same name

            :param django.http.HttpRequest request: The current request object
        """
219 220
        return super(FederateAuth, self).dispatch(request, *args, **kwargs)

Valentin Samir's avatar
Valentin Samir committed
221 222
    @staticmethod
    def get_cas_client(request, provider):
Valentin Samir's avatar
Valentin Samir committed
223 224 225 226 227 228
        """
            return a CAS client object matching provider

            :param django.http.HttpRequest request: The current request object
            :param cas_server.models.FederatedIendityProvider provider: the user identity provider
            :return: The user CAS client object
Valentin Samir's avatar
Valentin Samir committed
229 230
            :rtype: :class:`federate.CASFederateValidateUser
                <cas_server.federate.CASFederateValidateUser>`
Valentin Samir's avatar
Valentin Samir committed
231 232
        """
        # compute the current url, ignoring ticket dans provider GET parameters
233 234
        service_url = utils.get_current_url(request, {"ticket", "provider"})
        return CASFederateValidateUser(provider, service_url)
235

Valentin Samir's avatar
Valentin Samir committed
236
    def post(self, request, provider=None):
Valentin Samir's avatar
Valentin Samir committed
237 238 239 240 241 242 243
        """
            method called on POST request

            :param django.http.HttpRequest request: The current request object
            :param unicode provider: Optional parameter. The user provider suffix.
        """
        # if settings.CAS_FEDERATE is not True redirect to the login page
244
        if not settings.CAS_FEDERATE:
245
            logger.warning("CAS_FEDERATE is False, set it to True to use the federated mode")
246
            return redirect("cas_server:login")
Valentin Samir's avatar
Valentin Samir committed
247 248
        # POST with a provider suffix, this is probably an SLO request. csrf is disabled for
        # allowing SLO requests reception
249 250
        try:
            provider = FederatedIendityProvider.objects.get(suffix=provider)
251 252 253
            auth = self.get_cas_client(request, provider)
            try:
                auth.clean_sessions(request.POST['logoutRequest'])
254
            except (KeyError, AttributeError):
255 256 257
                pass
            return HttpResponse("ok")
        # else, a User is trying to log in using an identity provider
258
        except FederatedIendityProvider.DoesNotExist:
259 260
            # Manually checking for csrf to protect the code below
            reason = CsrfViewMiddleware().process_view(request, None, (), {})
261
            if reason is not None:  # pragma: no cover (csrf checks are disabled during tests)
262 263 264 265 266 267 268 269 270
                return reason  # Failed the test, stop here.
            form = forms.FederateSelect(request.POST)
            if form.is_valid():
                params = utils.copy_params(
                    request.POST,
                    ignore={"provider", "csrfmiddlewaretoken", "ticket"}
                )
                url = utils.reverse_params(
                    "cas_server:federateAuth",
271
                    kwargs=dict(provider=form.cleaned_data["provider"].suffix),
272 273 274
                    params=params
                )
                response = HttpResponseRedirect(url)
Valentin Samir's avatar
Valentin Samir committed
275
                # If the user has checked "remember my identity provider" store it in a cookie
276 277 278 279 280
                if form.cleaned_data["remember"]:
                    max_age = settings.CAS_FEDERATE_REMEMBER_TIMEOUT
                    utils.set_cookie(
                        response,
                        "_remember_provider",
281
                        form.cleaned_data["provider"].suffix,
282 283 284 285 286
                        max_age
                    )
                return response
            else:
                return redirect("cas_server:login")
Valentin Samir's avatar
Valentin Samir committed
287 288

    def get(self, request, provider=None):
Valentin Samir's avatar
Valentin Samir committed
289 290 291 292 293 294 295
        """
            method called on GET request

            :param django.http.HttpRequest request: The current request object
            :param unicode provider: Optional parameter. The user provider suffix.
        """
        # if settings.CAS_FEDERATE is not True redirect to the login page
296
        if not settings.CAS_FEDERATE:
297 298
            logger.warning("CAS_FEDERATE is False, set it to True to use the federated mode")
            return redirect("cas_server:login")
Valentin Samir's avatar
Valentin Samir committed
299 300
        # Is the user is already authenticated, no need to request authentication to the user
        # identity provider.
301 302
        if self.request.session.get("authenticated"):
            logger.warning("User already authenticated, dropping federate authentication request")
303
            return redirect("cas_server:login")
304
        try:
Valentin Samir's avatar
Valentin Samir committed
305
            # get the identity provider from its suffix
306
            provider = FederatedIendityProvider.objects.get(suffix=provider)
Valentin Samir's avatar
Valentin Samir committed
307
            # get a CAS client for the user identity provider
308
            auth = self.get_cas_client(request, provider)
Valentin Samir's avatar
Valentin Samir committed
309
            # if no ticket submited, redirect to the identity provider CAS login page
310
            if 'ticket' not in request.GET:
311
                logger.info("Trying to authenticate again %s" % auth.provider.server_url)
Valentin Samir's avatar
Valentin Samir committed
312
                return HttpResponseRedirect(auth.get_login_url())
313 314
            else:
                ticket = request.GET['ticket']
315 316 317 318 319 320 321 322
                try:
                    # if the ticket validation succeed
                    if auth.verify_ticket(ticket):
                        logger.info(
                            "Got a valid ticket for %s from %s" % (
                                auth.username,
                                auth.provider.server_url
                            )
323
                        )
324 325 326 327 328 329 330
                        params = utils.copy_params(request.GET, ignore={"ticket"})
                        request.session["federate_username"] = auth.federated_username
                        request.session["federate_ticket"] = ticket
                        auth.register_slo(
                            auth.federated_username,
                            request.session.session_key,
                            ticket
331
                        )
332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351
                        # redirect to the the login page for the user to become authenticated
                        # thanks to the `federate_username` and `federate_ticket` session parameters
                        url = utils.reverse_params("cas_server:login", params)
                        return HttpResponseRedirect(url)
                    # else redirect to the identity provider CAS login page
                    else:
                        logger.info(
                            "Got a invalid ticket for %s from %s. Retrying to authenticate" % (
                                auth.username,
                                auth.provider.server_url
                            )
                        )
                        return HttpResponseRedirect(auth.get_login_url())
                # both xml.etree.ElementTree and lxml.etree exceptions inherit from SyntaxError
                except SyntaxError as error:
                    messages.add_message(
                        request,
                        messages.ERROR,
                        _(
                            u"Invalid response from your identity provider CAS upon "
352 353
                            u"ticket %(ticket)s validation: %(error)r"
                        ) % {'ticket': ticket, 'error': error}
354
                    )
355 356 357
                    response = redirect("cas_server:login")
                    response.delete_cookie("_remember_provider")
                    return response
358
        except FederatedIendityProvider.DoesNotExist:
359
            logger.warning("Identity provider suffix %s not found" % provider)
Valentin Samir's avatar
Valentin Samir committed
360
            # if the identity provider is not found, redirect to the login page
361
            return redirect("cas_server:login")
Valentin Samir's avatar
Valentin Samir committed
362 363


Valentin Samir's avatar
Valentin Samir committed
364
class LoginView(View, LogoutMixin):
Valentin Samir's avatar
Valentin Samir committed
365
    """credential requestor / acceptor"""
366 367 368 369

    # pylint: disable=too-many-instance-attributes
    # Nine is reasonable in this case.

Valentin Samir's avatar
Valentin Samir committed
370
    #: The current :class:`models.User<cas_server.models.User>` object
Valentin Samir's avatar
Valentin Samir committed
371
    user = None
Valentin Samir's avatar
Valentin Samir committed
372
    #: The form to display to the user
Valentin Samir's avatar
Valentin Samir committed
373
    form = None
374

Valentin Samir's avatar
Valentin Samir committed
375
    #: current :class:`django.http.HttpRequest` object
376
    request = None
Valentin Samir's avatar
Valentin Samir committed
377
    #: service GET/POST parameter
378
    service = None
Valentin Samir's avatar
Valentin Samir committed
379
    #: ``True`` if renew GET/POST parameter is present and not "False"
380
    renew = None
Valentin Samir's avatar
Valentin Samir committed
381 382 383
    #: the warn GET/POST parameter
    warn = None
    #: the gateway GET/POST parameter
384
    gateway = None
Valentin Samir's avatar
Valentin Samir committed
385
    #: the method GET/POST parameter
386
    method = None
Valentin Samir's avatar
Valentin Samir committed
387 388 389

    #: ``True`` if the HTTP_X_AJAX http header is sent and ``settings.CAS_ENABLE_AJAX_AUTH``
    #: is ``True``, ``False`` otherwise.
390
    ajax = None
391

Valentin Samir's avatar
Valentin Samir committed
392
    #: ``True`` if the user has just authenticated
Valentin Samir's avatar
Valentin Samir committed
393
    renewed = False
Valentin Samir's avatar
Valentin Samir committed
394
    #: ``True`` if renew GET/POST parameter is present and not "False"
395
    warned = False
396

Valentin Samir's avatar
Valentin Samir committed
397 398
    #: The :class:`FederateAuth` transmited username (only used if ``settings.CAS_FEDERATE``
    #: is ``True``)
399
    username = None
Valentin Samir's avatar
Valentin Samir committed
400 401
    #: The :class:`FederateAuth` transmited ticket (only used if ``settings.CAS_FEDERATE`` is
    #: ``True``)
402
    ticket = None
403

Valentin Samir's avatar
Valentin Samir committed
404 405 406 407 408 409 410 411
    INVALID_LOGIN_TICKET = 1
    USER_LOGIN_OK = 2
    USER_LOGIN_FAILURE = 3
    USER_ALREADY_LOGGED = 4
    USER_AUTHENTICATED = 5
    USER_NOT_AUTHENTICATED = 6

    def init_post(self, request):
Valentin Samir's avatar
Valentin Samir committed
412 413 414 415 416
        """
            Initialize POST received parameters

            :param django.http.HttpRequest request: The current request object
        """
417 418
        self.request = request
        self.service = request.POST.get('service')
Valentin Samir's avatar
style  
Valentin Samir committed
419
        self.renew = bool(request.POST.get('renew') and request.POST['renew'] != "False")
420 421
        self.gateway = request.POST.get('gateway')
        self.method = request.POST.get('method')
422
        self.ajax = settings.CAS_ENABLE_AJAX_AUTH and 'HTTP_X_AJAX' in request.META
423 424
        if request.POST.get('warned') and request.POST['warned'] != "False":
            self.warned = True
Valentin Samir's avatar
Valentin Samir committed
425 426 427 428
        self.warn = request.POST.get('warn')
        if settings.CAS_FEDERATE:
            self.username = request.POST.get('username')
            self.ticket = request.POST.get('ticket')
429

430 431 432 433 434 435
    def gen_lt(self):
        """Generate a new LoginTicket and add it to the list of valid LT for the user"""
        self.request.session['lt'] = self.request.session.get('lt', []) + [utils.gen_lt()]
        if len(self.request.session['lt']) > 100:
            self.request.session['lt'] = self.request.session['lt'][-100:]

Valentin Samir's avatar
Valentin Samir committed
436
    def check_lt(self):
Valentin Samir's avatar
Valentin Samir committed
437 438 439 440 441 442
        """
            Check is the POSTed LoginTicket is valid, if yes invalide it

            :return: ``True`` if the LoginTicket is valid, ``False`` otherwise
            :rtype: bool
        """
443
        # save LT for later check
444
        lt_valid = self.request.session.get('lt', [])
Valentin Samir's avatar
Valentin Samir committed
445
        lt_send = self.request.POST.get('lt')
446
        # generate a new LT (by posting the LT has been consumed)
447
        self.gen_lt()
448
        # check if send LT is valid
449
        if lt_send not in lt_valid:
Valentin Samir's avatar
Valentin Samir committed
450 451
            return False
        else:
452
            self.request.session['lt'].remove(lt_send)
Valentin Samir's avatar
Valentin Samir committed
453 454
            # we need to redo the affectation for django to detect that the list has changed
            # and for its new value to be store in the session
455
            self.request.session['lt'] = self.request.session['lt']
Valentin Samir's avatar
Valentin Samir committed
456 457 458
            return True

    def post(self, request, *args, **kwargs):
Valentin Samir's avatar
Valentin Samir committed
459 460 461 462 463 464
        """
            methode called on POST request on this view

            :param django.http.HttpRequest request: The current request object
        """
        # initialize class parameters
Valentin Samir's avatar
Valentin Samir committed
465
        self.init_post(request)
Valentin Samir's avatar
Valentin Samir committed
466
        # process the POST request
Valentin Samir's avatar
Valentin Samir committed
467 468
        ret = self.process_post()
        if ret == self.INVALID_LOGIN_TICKET:
469 470 471
            messages.add_message(
                self.request,
                messages.ERROR,
472
                _(u"Invalid login ticket, please retry to login")
Valentin Samir's avatar
Valentin Samir committed
473
            )
Valentin Samir's avatar
Valentin Samir committed
474
        elif ret == self.USER_LOGIN_OK:
Valentin Samir's avatar
Valentin Samir committed
475 476
            # On successful login, update the :class:`models.User<cas_server.models.User>` ``date``
            # attribute by saving it. (``auto_now=True``)
477 478 479 480 481
            self.user = models.User.objects.get_or_create(
                username=self.request.session['username'],
                session_key=self.request.session.session_key
            )[0]
            self.user.save()
Valentin Samir's avatar
Valentin Samir committed
482
        elif ret == self.USER_LOGIN_FAILURE:  # bad user login
483 484
            if settings.CAS_FEDERATE:
                self.ticket = None
485
                self.username = None
486
                self.init_form()
487 488
            # preserve valid LoginTickets from session flush
            lt = self.request.session.get('lt', [])
Valentin Samir's avatar
Valentin Samir committed
489
            # On login failure, flush the session
Valentin Samir's avatar
Valentin Samir committed
490
            self.logout()
491 492
            # restore valid LoginTickets
            self.request.session['lt'] = lt
Valentin Samir's avatar
Valentin Samir committed
493 494
        elif ret == self.USER_ALREADY_LOGGED:
            pass
Valentin Samir's avatar
Valentin Samir committed
495 496 497
        else:  # pragma: no cover (should no happen)
            raise EnvironmentError("invalid output for LoginView.process_post")
        # call the GET/POST common part
Valentin Samir's avatar
Valentin Samir committed
498 499
        return self.common()

Valentin Samir's avatar
style  
Valentin Samir committed
500
    def process_post(self):
501 502
        """
            Analyse the POST request:
Valentin Samir's avatar
Valentin Samir committed
503

504 505
                * check that the LoginTicket is valid
                * check that the user sumited credentials are valid
Valentin Samir's avatar
Valentin Samir committed
506 507 508 509 510 511 512 513 514 515

            :return:
                * :attr:`INVALID_LOGIN_TICKET` if the POSTed LoginTicket is not valid
                * :attr:`USER_ALREADY_LOGGED` if the user is already logged and do no request
                  reauthentication.
                * :attr:`USER_LOGIN_FAILURE` if the user is not logged or request for
                  reauthentication and his credentials are not valid
                * :attr:`USER_LOGIN_OK` if the user is not logged or request for
                  reauthentication and his credentials are valid
            :rtype: int
516
        """
Valentin Samir's avatar
Valentin Samir committed
517
        if not self.check_lt():
518
            self.init_form(self.request.POST)
Valentin Samir's avatar
Valentin Samir committed
519
            logger.warning("Receive an invalid login ticket")
Valentin Samir's avatar
Valentin Samir committed
520 521
            return self.INVALID_LOGIN_TICKET
        elif not self.request.session.get("authenticated") or self.renew:
Valentin Samir's avatar
Valentin Samir committed
522
            # authentication request receive, initialize the form to use
Valentin Samir's avatar
Valentin Samir committed
523 524 525 526 527 528
            self.init_form(self.request.POST)
            if self.form.is_valid():
                self.request.session.set_expiry(0)
                self.request.session["username"] = self.form.cleaned_data['username']
                self.request.session["warn"] = True if self.form.cleaned_data.get("warn") else False
                self.request.session["authenticated"] = True
529 530
                self.renewed = True
                self.warned = True
Valentin Samir's avatar
Valentin Samir committed
531
                logger.info("User %s successfully authenticated" % self.request.session["username"])
Valentin Samir's avatar
Valentin Samir committed
532
                return self.USER_LOGIN_OK
Valentin Samir's avatar
Valentin Samir committed
533
            else:
Valentin Samir's avatar
Valentin Samir committed
534
                logger.warning("A logging attemps failed")
Valentin Samir's avatar
Valentin Samir committed
535 536
                return self.USER_LOGIN_FAILURE
        else:
Valentin Samir's avatar
Valentin Samir committed
537
            logger.warning("Receuve a logging attempt whereas the user is already logged")
Valentin Samir's avatar
Valentin Samir committed
538
            return self.USER_ALREADY_LOGGED
539

Valentin Samir's avatar
Valentin Samir committed
540
    def init_get(self, request):
Valentin Samir's avatar
Valentin Samir committed
541 542 543 544 545
        """
            Initialize GET received parameters

            :param django.http.HttpRequest request: The current request object
        """
546 547
        self.request = request
        self.service = request.GET.get('service')
Valentin Samir's avatar
style  
Valentin Samir committed
548
        self.renew = bool(request.GET.get('renew') and request.GET['renew'] != "False")
549 550
        self.gateway = request.GET.get('gateway')
        self.method = request.GET.get('method')
551
        self.ajax = settings.CAS_ENABLE_AJAX_AUTH and 'HTTP_X_AJAX' in request.META
Valentin Samir's avatar
Valentin Samir committed
552 553
        self.warn = request.GET.get('warn')
        if settings.CAS_FEDERATE:
Valentin Samir's avatar
Valentin Samir committed
554 555
            # here username and ticket are fetch from the session after a redirection from
            # FederateAuth.get
556 557 558 559 560 561
            self.username = request.session.get("federate_username")
            self.ticket = request.session.get("federate_ticket")
            if self.username:
                del request.session["federate_username"]
            if self.ticket:
                del request.session["federate_ticket"]
562

Valentin Samir's avatar
Valentin Samir committed
563
    def get(self, request, *args, **kwargs):
Valentin Samir's avatar
Valentin Samir committed
564 565 566 567 568 569
        """
            methode called on GET request on this view

            :param django.http.HttpRequest request: The current request object
        """
        # initialize class parameters
Valentin Samir's avatar
Valentin Samir committed
570
        self.init_get(request)
Valentin Samir's avatar
Valentin Samir committed
571
        # process the GET request
Valentin Samir's avatar
Valentin Samir committed
572
        self.process_get()
Valentin Samir's avatar
Valentin Samir committed
573
        # call the GET/POST common part
Valentin Samir's avatar
Valentin Samir committed
574 575 576
        return self.common()

    def process_get(self):
Valentin Samir's avatar
Valentin Samir committed
577 578 579 580 581 582 583 584 585 586
        """
            Analyse the GET request

            :return:
                * :attr:`USER_NOT_AUTHENTICATED` if the user is not authenticated or is requesting
                  for authentication renewal
                * :attr:`USER_AUTHENTICATED` if the user is authenticated and is not requesting
                  for authentication renewal
            :rtype: int
        """
587 588
        # generate a new LT
        self.gen_lt()
Valentin Samir's avatar
Valentin Samir committed
589
        if not self.request.session.get("authenticated") or self.renew:
Valentin Samir's avatar
Valentin Samir committed
590
            # authentication will be needed, initialize the form to use
591
            self.init_form()
Valentin Samir's avatar
Valentin Samir committed
592 593
            return self.USER_NOT_AUTHENTICATED
        return self.USER_AUTHENTICATED
Valentin Samir's avatar
Valentin Samir committed
594

595
    def init_form(self, values=None):
Valentin Samir's avatar
Valentin Samir committed
596 597 598 599 600
        """
            Initialization of the good form depending of POST and GET parameters

            :param django.http.QueryDict values: A POST or GET QueryDict
        """
601 602 603
        if values:
            values = values.copy()
            values['lt'] = self.request.session['lt'][-1]
Valentin Samir's avatar
Valentin Samir committed
604 605 606 607
        form_initial = {
            'service': self.service,
            'method': self.method,
            'warn': self.warn or self.request.session.get("warn"),
608 609
            'lt': self.request.session['lt'][-1],
            'renew': self.renew
Valentin Samir's avatar
Valentin Samir committed
610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626
        }
        if settings.CAS_FEDERATE:
            if self.username and self.ticket:
                form_initial['username'] = self.username
                form_initial['password'] = self.ticket
                form_initial['ticket'] = self.ticket
                self.form = forms.FederateUserCredential(
                    values,
                    initial=form_initial
                )
            else:
                self.form = forms.FederateSelect(values, initial=form_initial)
        else:
            self.form = forms.UserCredential(
                values,
                initial=form_initial
            )
627

628
    def service_login(self):
Valentin Samir's avatar
Valentin Samir committed
629 630 631 632 633 634 635 636 637 638 639 640 641
        """
            Perform login agains a service

            :return:
                * The rendering of the ``settings.CAS_WARN_TEMPLATE`` if the user asked to be
                  warned before ticket emission and has not yep been warned.
                * The redirection to the service URL with a ticket GET parameter
                * The redirection to the service URL without a ticket if ticket generation failed
                  and the :attr:`gateway` attribute is set
                * The rendering of the ``settings.CAS_LOGGED_TEMPLATE`` template with some error
                  messages if the ticket generation failed (e.g: user not allowed).
            :rtype: django.http.HttpResponse
        """
642 643
        try:
            # is the service allowed
Valentin Samir's avatar
Valentin Samir committed
644
            service_pattern = ServicePattern.validate(self.service)
645 646 647 648
            # is the current user allowed on this service
            service_pattern.check_user(self.user)
            # if the user has asked to be warned before any login to a service
            if self.request.session.get("warn", True) and not self.warned:
Valentin Samir's avatar
Valentin Samir committed
649
                messages.add_message(
650 651
                    self.request,
                    messages.WARNING,
Valentin Samir's avatar
PEP8  
Valentin Samir committed
652 653
                    _(u"Authentication has been required by service %(name)s (%(url)s)") %
                    {'name': service_pattern.name, 'url': self.service}
Valentin Samir's avatar
Valentin Samir committed
654
                )
655 656
                if self.ajax:
                    data = {"status": "error", "detail": "confirmation needed"}
Valentin Samir's avatar
style  
Valentin Samir committed
657
                    return json_response(self.request, data)
658
                else:
659 660 661 662 663 664 665 666
                    warn_form = forms.WarnForm(initial={
                        'service': self.service,
                        'renew': self.renew,
                        'gateway': self.gateway,
                        'method': self.method,
                        'warned': True,
                        'lt': self.request.session['lt'][-1]
                    })
667 668 669
                    return render(
                        self.request,
                        settings.CAS_WARN_TEMPLATE,
670
                        utils.context({'form': warn_form})
Valentin Samir's avatar
Valentin Samir committed
671
                    )
672 673
            else:
                # redirect, using method ?
Valentin Samir's avatar
PEP8  
Valentin Samir committed
674
                list(messages.get_messages(self.request))  # clean messages before leaving django
675 676 677
                redirect_url = self.user.get_service_url(
                    self.service,
                    service_pattern,
678
                    renew=self.renewed
Valentin Samir's avatar
Valentin Samir committed
679
                )
680 681 682 683
                if not self.ajax:
                    return HttpResponseRedirect(redirect_url)
                else:
                    data = {"status": "success", "detail": "auth", "url": redirect_url}
Valentin Samir's avatar
style  
Valentin Samir committed
684
                    return json_response(self.request, data)
Valentin Samir's avatar
Valentin Samir committed
685
        except ServicePattern.DoesNotExist:
686
            error = 1
687 688 689
            messages.add_message(
                self.request,
                messages.ERROR,
Valentin Samir's avatar
PEP8  
Valentin Samir committed
690
                _(u'Service %(url)s non allowed.') % {'url': self.service}
691 692
            )
        except models.BadUsername:
693
            error = 2
694 695 696 697 698 699
            messages.add_message(
                self.request,
                messages.ERROR,
                _(u"Username non allowed")
            )
        except models.BadFilter:
700
            error = 3
701 702 703
            messages.add_message(
                self.request,
                messages.ERROR,
704
                _(u"User characteristics non allowed")
705 706
            )
        except models.UserFieldNotDefined:
707
            error = 4
708 709 710
            messages.add_message(
                self.request,
                messages.ERROR,
711
                _(u"The attribute %(field)s is needed to use"
Valentin Samir's avatar
PEP8  
Valentin Samir committed
712
                  u" that service") % {'field': service_pattern.user_field}
713 714 715
            )

        # if gateway is set and auth failed redirect to the service without authentication
716
        if self.gateway and not self.ajax:
Valentin Samir's avatar
PEP8  
Valentin Samir committed
717
            list(messages.get_messages(self.request))  # clean messages before leaving django
718
            return HttpResponseRedirect(self.service)
Valentin Samir's avatar
Valentin Samir committed
719

720 721 722 723
        if not self.ajax:
            return render(
                self.request,
                settings.CAS_LOGGED_TEMPLATE,
724
                utils.context({'session': self.request.session})
725 726 727
            )
        else:
            data = {"status": "error", "detail": "auth", "code": error}
Valentin Samir's avatar
style  
Valentin Samir committed
728
            return json_response(self.request, data)
Valentin Samir's avatar
Valentin Samir committed
729

730
    def authenticated(self):
Valentin Samir's avatar
Valentin Samir committed
731 732 733 734 735 736 737 738 739 740
        """
            Processing authenticated users

            :return:
                * The returned value of :meth:`service_login` if :attr:`service` is defined
                * The rendering of ``settings.CAS_LOGGED_TEMPLATE`` otherwise
            :rtype: django.http.HttpResponse
        """
        # Try to get the current :class:`models.User<cas_server.models.User>` object for the current
        # session
741
        try:
Valentin Samir's avatar
Valentin Samir committed
742 743
            self.user = models.User.objects.get(
                username=self.request.session.get("username"),
Valentin Samir's avatar
oops  
Valentin Samir committed
744
                session_key=self.request.session.session_key
Valentin Samir's avatar
Valentin Samir committed
745
            )
Valentin Samir's avatar
Valentin Samir committed
746
        # if not found, flush the session and redirect to the login page
747
        except models.User.DoesNotExist:
Valentin Samir's avatar
Valentin Samir committed
748 749 750 751 752
            logger.warning(
                "User %s seems authenticated but is not found in the database." % (
                    self.request.session.get("username"),
                )
            )
753
            self.logout()
754 755 756 757 758 759
            if self.ajax:
                data = {
                    "status": "error",
                    "detail": "login required",
                    "url": utils.reverse_params("cas_server:login", params=self.request.GET)
                }
Valentin Samir's avatar
style  
Valentin Samir committed
760
                return json_response(self.request, data)
761 762
            else:
                return utils.redirect_params("cas_server:login", params=self.request.GET)
763

Valentin Samir's avatar
Valentin Samir committed
764
        # if login agains a service
765 766
        if self.service:
            return self.service_login()
Valentin Samir's avatar
Valentin Samir committed
767
        # else display the logged template
768
        else:
769 770
            if self.ajax:
                data = {"status": "success", "detail": "logged"}
Valentin Samir's avatar
style  
Valentin Samir committed
771
                return json_response(self.request, data)
772 773 774 775
            else:
                return render(
                    self.request,
                    settings.CAS_LOGGED_TEMPLATE,
776
                    utils.context({'session': self.request.session})
777
                )
778 779

    def not_authenticated(self):
Valentin Samir's avatar
Valentin Samir committed
780 781 782 783 784 785 786 787 788 789
        """
            Processing non authenticated users

            :return:
                * The rendering of ``settings.CAS_LOGIN_TEMPLATE`` with various messages
                  depending of GET/POST parameters
                * The redirection to :class:`FederateAuth` if ``settings.CAS_FEDERATE`` is ``True``
                  and the "remember my identity provider" cookie is found
            :rtype: django.http.HttpResponse
        """
790
        if self.service:
Valentin Samir's avatar
Valentin Samir committed
791
            try:
Valentin Samir's avatar
Valentin Samir committed
792
                service_pattern = ServicePattern.validate(self.service)
793
                if self.gateway and not self.ajax:
Valentin Samir's avatar
PEP8  
Valentin Samir committed
794 795
                    # clean messages before leaving django
                    list(messages.get_messages(self.request))
796 797
                    return HttpResponseRedirect(self.service)
                if self.request.session.get("authenticated") and self.renew:
Valentin Samir's avatar
Valentin Samir committed
798
                    messages.add_message(
799
                        self.request,
Valentin Samir's avatar
Valentin Samir committed
800
                        messages.WARNING,
801
                        _(u"Authentication renewal required by service %(name)s (%(url)s).") %
Valentin Samir's avatar
PEP8  
Valentin Samir committed
802
                        {'name': service_pattern.name, 'url': self.service}
Valentin Samir's avatar
Valentin Samir committed
803
                    )
Valentin Samir's avatar
Valentin Samir committed
804
                else:
Valentin Samir's avatar
Valentin Samir committed
805
                    messages.add_message(
806
                        self.request,
Valentin Samir's avatar
Valentin Samir committed
807
                        messages.WARNING,
808
                        _(u"Authentication required by service %(name)s (%(url)s).") %
Valentin Samir's avatar
PEP8  
Valentin Samir committed
809
                        {'name': service_pattern.name, 'url': self.service}
Valentin Samir's avatar
Valentin Samir committed
810
                    )
Valentin Samir's avatar
Valentin Samir committed
811
            except ServicePattern.DoesNotExist:
Valentin Samir's avatar
Valentin Samir committed
812
                messages.add_message(
813
                    self.request,
Valentin Samir's avatar
Valentin Samir committed
814
                    messages.ERROR,
815
                    _(u'Service %s non allowed') % self.service
Valentin Samir's avatar
Valentin Samir committed
816
                )
817 818 819 820 821 822
        if self.ajax:
            data = {
                "status": "error",
                "detail": "login required",
                "url": utils.reverse_params("cas_server:login",  params=self.request.GET)
            }
Valentin Samir's avatar
style  
Valentin Samir committed
823
            return json_response(self.request, data)
824
        else:
Valentin Samir's avatar
Valentin Samir committed
825 826 827 828 829
            if settings.CAS_FEDERATE:
                if self.username and self.ticket:
                    return render(
                        self.request,
                        settings.CAS_LOGIN_TEMPLATE,
830
                        utils.context({
Valentin Samir's avatar
Valentin Samir committed
831 832 833
                            'form': self.form,
                            'auto_submit': True,
                            'post_url': reverse("cas_server:login")
834
                        })
Valentin Samir's avatar
Valentin Samir committed
835 836 837 838
                    )
                else:
                    if (
                        self.request.COOKIES.get('_remember_provider') and
839 840 841
                        FederatedIendityProvider.objects.filter(
                            suffix=self.request.COOKIES['_remember_provider']
                        )
Valentin Samir's avatar
Valentin Samir committed
842 843 844 845 846 847 848 849 850 851 852
                    ):
                        params = utils.copy_params(self.request.GET)
                        url = utils.reverse_params(
                            "cas_server:federateAuth",
                            params=params,
                            kwargs=dict(provider=self.request.COOKIES['_remember_provider'])
                        )
                        return HttpResponseRedirect(url)
                    else:
                        return render(
                            self.request,
853 854 855 856 857
                            settings.CAS_LOGIN_TEMPLATE,
                            utils.context({
                                'form': self.form,
                                'post_url': reverse("cas_server:federateAuth")
                            })
Valentin Samir's avatar
Valentin Samir committed
858 859
                        )
            else:
860