Maintenance électrique le 12 août, nos services, et y compris Gitlab, seront fortement impactés autour de cette date. Retour à un état normal prévu dans le milieu de la semaine prochaine.

views.py 60.1 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
Valentin Samir's avatar
Valentin Samir committed
16
from django.http import HttpResponse, HttpResponseRedirect
Valentin Samir's avatar
Valentin Samir committed
17
from django.contrib import messages
18
from django.utils.decorators import method_decorator
Valentin Samir's avatar
Valentin Samir committed
19
from django.utils.translation import ugettext as _
Valentin Samir's avatar
Valentin Samir committed
20
from django.utils import timezone
21
from django.views.decorators.csrf import csrf_exempt
22
from django.middleware.csrf import CsrfViewMiddleware
23
from django.views.generic import View
24
from django.utils.encoding import python_2_unicode_compatible
Valentin Samir's avatar
Valentin Samir committed
25
from django.utils.safestring import mark_safe
26 27 28 29
try:
    from django.urls import reverse
except ImportError:
    from django.core.urlresolvers import reverse
Valentin Samir's avatar
Valentin Samir committed
30

31
import re
Valentin Samir's avatar
Valentin Samir committed
32 33
import logging
import pprint
Valentin Samir's avatar
Valentin Samir committed
34
import requests
Valentin Samir's avatar
Valentin Samir committed
35
from lxml import etree
Valentin Samir's avatar
Valentin Samir committed
36
from datetime import timedelta
Valentin Samir's avatar
Valentin Samir committed
37

Valentin Samir's avatar
Valentin Samir committed
38 39 40
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
41

Valentin Samir's avatar
style  
Valentin Samir committed
42
from .utils import json_response
Valentin Samir's avatar
Valentin Samir committed
43
from .models import Ticket, ServiceTicket, ProxyTicket, ProxyGrantingTicket
44
from .models import ServicePattern, FederatedIendityProvider, FederatedUser
Valentin Samir's avatar
Valentin Samir committed
45
from .federate import CASFederateValidateUser
Valentin Samir's avatar
Valentin Samir committed
46

Valentin Samir's avatar
Valentin Samir committed
47 48
logger = logging.getLogger(__name__)

Valentin Samir's avatar
PEP8  
Valentin Samir committed
49

Valentin Samir's avatar
Valentin Samir committed
50
class LogoutMixin(object):
51
    """destroy CAS session utils"""
52

Valentin Samir's avatar
style  
Valentin Samir committed
53
    def logout(self, all_session=False):
Valentin Samir's avatar
Valentin Samir committed
54 55 56 57 58 59 60 61 62
        """
            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
63
        session_nb = 0
Valentin Samir's avatar
Valentin Samir committed
64
        # save the current user username before flushing the session
Valentin Samir's avatar
Valentin Samir committed
65 66
        username = self.request.session.get("username")
        if username:
Valentin Samir's avatar
style  
Valentin Samir committed
67
            if all_session:
68
                logger.info("Logging out user %s from all sessions." % username)
Valentin Samir's avatar
Valentin Samir committed
69 70
            else:
                logger.info("Logging out user %s." % username)
71 72
        users = []
        # try to get the user from the current session
73
        try:
74 75 76 77 78
            users.append(
                models.User.objects.get(
                    username=username,
                    session_key=self.request.session.session_key
                )
Valentin Samir's avatar
Valentin Samir committed
79
            )
80 81
        except models.User.DoesNotExist:
            # if user not found in database, flush the session anyway
82
            self.request.session.flush()
83 84 85

        # If all_session is set, search all of the user sessions
        if all_session:
86 87 88 89 90 91 92
            users.extend(
                models.User.objects.filter(
                    username=username
                ).exclude(
                    session_key=self.request.session.session_key
                )
            )
93 94 95 96 97 98 99

        # Iterate over all user sessions that have to be logged out
        for user in users:
            # get the user session
            session = SessionStore(session_key=user.session_key)
            # flush the session
            session.flush()
Valentin Samir's avatar
Valentin Samir committed
100
            # send SLO requests
101
            user.logout(self.request)
Valentin Samir's avatar
Valentin Samir committed
102
            # delete the user
103
            user.delete()
Valentin Samir's avatar
Valentin Samir committed
104
            # increment the destroyed session counter
105
            session_nb += 1
Valentin Samir's avatar
Valentin Samir committed
106 107
        if username:
            logger.info("User %s logged out" % username)
108
        return session_nb
109

Valentin Samir's avatar
PEP8  
Valentin Samir committed
110

111 112 113 114 115 116 117 118 119 120 121 122 123
class CsrfExemptView(View):
    """base class for csrf exempt class views"""

    @method_decorator(csrf_exempt)  # csrf is disabled for allowing SLO requests reception
    def dispatch(self, request, *args, **kwargs):
        """
            dispatch different http request to the methods of the same name

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


Valentin Samir's avatar
Valentin Samir committed
124 125 126
class LogoutView(View, LogoutMixin):
    """destroy CAS session (logout) view"""

Valentin Samir's avatar
Valentin Samir committed
127
    #: current :class:`django.http.HttpRequest` object
Valentin Samir's avatar
Valentin Samir committed
128
    request = None
Valentin Samir's avatar
Valentin Samir committed
129
    #: service GET parameter
Valentin Samir's avatar
Valentin Samir committed
130
    service = None
Valentin Samir's avatar
Valentin Samir committed
131 132 133 134 135
    #: 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
136

Valentin Samir's avatar
Valentin Samir committed
137
    def init_get(self, request):
Valentin Samir's avatar
Valentin Samir committed
138 139 140 141 142
        """
            Initialize the :class:`LogoutView` attributes on GET request

            :param django.http.HttpRequest request: The current request object
        """
143 144
        self.request = request
        self.service = request.GET.get('service')
Valentin Samir's avatar
Valentin Samir committed
145
        self.url = request.GET.get('url')
146
        self.ajax = settings.CAS_ENABLE_AJAX_AUTH and 'HTTP_X_AJAX' in request.META
Valentin Samir's avatar
Valentin Samir committed
147 148

    def get(self, request, *args, **kwargs):
Valentin Samir's avatar
Valentin Samir committed
149
        """
150
            method called on GET request on this view
Valentin Samir's avatar
Valentin Samir committed
151 152 153

            :param django.http.HttpRequest request: The current request object
        """
Valentin Samir's avatar
Valentin Samir committed
154
        logger.info("logout requested")
Valentin Samir's avatar
Valentin Samir committed
155
        # initialize the class attributes
Valentin Samir's avatar
Valentin Samir committed
156
        self.init_get(request)
Valentin Samir's avatar
Valentin Samir committed
157 158
        # if CAS federation mode is enable, bakup the provider before flushing the sessions
        if settings.CAS_FEDERATE:
159 160 161 162 163 164
            try:
                user = FederatedUser.get_from_federated_username(
                    self.request.session.get("username")
                )
                auth = CASFederateValidateUser(user.provider, service_url="")
            except FederatedUser.DoesNotExist:
165
                auth = None
166
        session_nb = self.logout(self.request.GET.get("all"))
Valentin Samir's avatar
Valentin Samir committed
167 168
        # if CAS federation mode is enable, redirect to user CAS logout page, appending the
        # current querystring
Valentin Samir's avatar
Valentin Samir committed
169
        if settings.CAS_FEDERATE:
170
            if auth is not None:
171
                params = utils.copy_params(request.GET, ignore={"forget_provider"})
172
                url = auth.get_logout_url()
173 174 175 176
                response = HttpResponseRedirect(utils.update_url(url, params))
                if request.GET.get("forget_provider"):
                    response.delete_cookie("remember_provider")
                return response
177 178
        # if service is set, redirect to service after logout
        if self.service:
Valentin Samir's avatar
PEP8  
Valentin Samir committed
179
            list(messages.get_messages(request))  # clean messages before leaving the django app
180
            return HttpResponseRedirect(self.service)
Valentin Samir's avatar
Valentin Samir committed
181
        # if service is not set but url is set, redirect to url after logout
Valentin Samir's avatar
Valentin Samir committed
182
        elif self.url:
Valentin Samir's avatar
PEP8  
Valentin Samir committed
183
            list(messages.get_messages(request))  # clean messages before leaving the django app
Valentin Samir's avatar
Valentin Samir committed
184
            return HttpResponseRedirect(self.url)
185
        else:
Valentin Samir's avatar
Valentin Samir committed
186
            # build logout message depending of the number of sessions the user logs out
187
            if session_nb == 1:
Valentin Samir's avatar
Valentin Samir committed
188
                logout_msg = mark_safe(_(
189 190
                    "<h3>Logout successful</h3>"
                    "You have successfully logged out from the Central Authentication Service. "
191
                    "For security reasons, close your web browser."
Valentin Samir's avatar
Valentin Samir committed
192
                ))
193
            elif session_nb > 1:
Valentin Samir's avatar
Valentin Samir committed
194
                logout_msg = mark_safe(_(
195
                    "<h3>Logout successful</h3>"
Valentin Samir's avatar
Valentin Samir committed
196
                    "You have successfully logged out from %d sessions of the Central "
197
                    "Authentication Service. "
198
                    "For security reasons, close your web browser."
Valentin Samir's avatar
Valentin Samir committed
199
                ) % session_nb)
200
            else:
Valentin Samir's avatar
Valentin Samir committed
201
                logout_msg = mark_safe(_(
202 203
                    "<h3>Logout successful</h3>"
                    "You were already logged out from the Central Authentication Service. "
204
                    "For security reasons, close your web browser."
Valentin Samir's avatar
Valentin Samir committed
205
                ))
206

Valentin Samir's avatar
Valentin Samir committed
207 208
            # 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.
209
            if settings.CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT:
210
                messages.add_message(request, messages.SUCCESS, logout_msg)
211 212
                if self.ajax:
                    url = reverse("cas_server:login")
213 214 215 216 217 218
                    data = {
                        'status': 'success',
                        'detail': 'logout',
                        'url': url,
                        'session_nb': session_nb
                    }
Valentin Samir's avatar
style  
Valentin Samir committed
219
                    return json_response(request, data)
220 221
                else:
                    return redirect("cas_server:login")
222
            else:
223
                if self.ajax:
224
                    data = {'status': 'success', 'detail': 'logout', 'session_nb': session_nb}
Valentin Samir's avatar
style  
Valentin Samir committed
225
                    return json_response(request, data)
226
                else:
227 228 229
                    return render(
                        request,
                        settings.CAS_LOGOUT_TEMPLATE,
230
                        utils.context({'logout_msg': logout_msg})
231
                    )
232

Valentin Samir's avatar
PEP8  
Valentin Samir committed
233

234 235
class FederateAuth(CsrfExemptView):
    """
236
        view to authenticated user against a backend CAS then CAS_FEDERATE is True
Valentin Samir's avatar
Valentin Samir committed
237

238 239
        csrf is disabled for allowing SLO requests reception.
    """
240

241 242 243
    #: current URL used as service URL by the CAS client
    service_url = None

244
    def get_cas_client(self, request, provider, renew=False):
Valentin Samir's avatar
Valentin Samir committed
245 246 247 248 249 250
        """
            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
251 252
            :rtype: :class:`federate.CASFederateValidateUser
                <cas_server.federate.CASFederateValidateUser>`
Valentin Samir's avatar
Valentin Samir committed
253 254
        """
        # compute the current url, ignoring ticket dans provider GET parameters
255
        service_url = utils.get_current_url(request, {"ticket", "provider"})
256
        self.service_url = service_url
257
        return CASFederateValidateUser(provider, service_url, renew=renew)
258

Valentin Samir's avatar
Valentin Samir committed
259
    def post(self, request, provider=None):
Valentin Samir's avatar
Valentin Samir committed
260 261 262 263 264 265 266
        """
            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
267
        if not settings.CAS_FEDERATE:
268
            logger.warning("CAS_FEDERATE is False, set it to True to use federation")
269
            return redirect("cas_server:login")
Valentin Samir's avatar
Valentin Samir committed
270 271
        # POST with a provider suffix, this is probably an SLO request. csrf is disabled for
        # allowing SLO requests reception
272 273
        try:
            provider = FederatedIendityProvider.objects.get(suffix=provider)
274 275 276
            auth = self.get_cas_client(request, provider)
            try:
                auth.clean_sessions(request.POST['logoutRequest'])
277
            except (KeyError, AttributeError):
278 279 280
                pass
            return HttpResponse("ok")
        # else, a User is trying to log in using an identity provider
281
        except FederatedIendityProvider.DoesNotExist:
282 283
            # Manually checking for csrf to protect the code below
            reason = CsrfViewMiddleware().process_view(request, None, (), {})
284
            if reason is not None:  # pragma: no cover (csrf checks are disabled during tests)
285 286 287 288 289
                return reason  # Failed the test, stop here.
            form = forms.FederateSelect(request.POST)
            if form.is_valid():
                params = utils.copy_params(
                    request.POST,
290
                    ignore={"provider", "csrfmiddlewaretoken", "ticket", "lt"}
291
                )
Valentin Samir's avatar
Valentin Samir committed
292 293
                if params.get("renew") == "False":
                    del params["renew"]
294 295
                url = utils.reverse_params(
                    "cas_server:federateAuth",
296
                    kwargs=dict(provider=form.cleaned_data["provider"].suffix),
297 298
                    params=params
                )
299
                return HttpResponseRedirect(url)
300 301
            else:
                return redirect("cas_server:login")
Valentin Samir's avatar
Valentin Samir committed
302 303

    def get(self, request, provider=None):
Valentin Samir's avatar
Valentin Samir committed
304 305 306
        """
            method called on GET request

307
            :param django.http.HttpRequestself. request: The current request object
Valentin Samir's avatar
Valentin Samir committed
308 309 310
            :param unicode provider: Optional parameter. The user provider suffix.
        """
        # if settings.CAS_FEDERATE is not True redirect to the login page
311
        if not settings.CAS_FEDERATE:
312
            logger.warning("CAS_FEDERATE is False, set it to True to use federation")
313
            return redirect("cas_server:login")
314
        renew = bool(request.GET.get('renew') and request.GET['renew'] != "False")
Valentin Samir's avatar
Valentin Samir committed
315 316
        # Is the user is already authenticated, no need to request authentication to the user
        # identity provider.
317
        if self.request.session.get("authenticated") and not renew:
318
            logger.warning("User already authenticated, dropping federated authentication request")
319
            return redirect("cas_server:login")
320
        try:
Valentin Samir's avatar
Valentin Samir committed
321
            # get the identity provider from its suffix
322
            provider = FederatedIendityProvider.objects.get(suffix=provider)
Valentin Samir's avatar
Valentin Samir committed
323
            # get a CAS client for the user identity provider
324
            auth = self.get_cas_client(request, provider, renew)
Valentin Samir's avatar
Valentin Samir committed
325
            # if no ticket submited, redirect to the identity provider CAS login page
326
            if 'ticket' not in request.GET:
327
                logger.info("Trying to authenticate %s again" % auth.provider.server_url)
Valentin Samir's avatar
Valentin Samir committed
328
                return HttpResponseRedirect(auth.get_login_url())
329 330
            else:
                ticket = request.GET['ticket']
331 332 333 334 335 336 337 338
                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
                            )
339
                        )
340
                        params = utils.copy_params(request.GET, ignore={"ticket", "remember"})
341 342 343 344 345 346
                        request.session["federate_username"] = auth.federated_username
                        request.session["federate_ticket"] = ticket
                        auth.register_slo(
                            auth.federated_username,
                            request.session.session_key,
                            ticket
347
                        )
348 349 350
                        # 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)
351 352 353 354 355 356 357
                        response = HttpResponseRedirect(url)
                        # If the user has checked "remember my identity provider" store it in a
                        # cookie
                        if request.GET.get("remember"):
                            max_age = settings.CAS_FEDERATE_REMEMBER_TIMEOUT
                            utils.set_cookie(
                                response,
358
                                "remember_provider",
359 360 361 362
                                provider.suffix,
                                max_age
                            )
                        return response
363 364 365
                    # else redirect to the identity provider CAS login page
                    else:
                        logger.info(
366
                            (
367 368
                                "Got an invalid ticket %s from %s for service %s. "
                                "Retrying authentication"
369 370 371 372
                            ) % (
                                ticket,
                                auth.provider.server_url,
                                self.service_url
373 374 375 376 377 378 379 380 381 382
                            )
                        )
                        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 "
383 384
                            u"ticket %(ticket)s validation: %(error)r"
                        ) % {'ticket': ticket, 'error': error}
385
                    )
386
                    response = redirect("cas_server:login")
387
                    response.delete_cookie("remember_provider")
388
                    return response
389
        except FederatedIendityProvider.DoesNotExist:
390
            logger.warning("Identity provider suffix %s not found" % provider)
Valentin Samir's avatar
Valentin Samir committed
391
            # if the identity provider is not found, redirect to the login page
392
            return redirect("cas_server:login")
Valentin Samir's avatar
Valentin Samir committed
393 394


Valentin Samir's avatar
Valentin Samir committed
395
class LoginView(View, LogoutMixin):
Valentin Samir's avatar
Valentin Samir committed
396
    """credential requestor / acceptor"""
397 398 399 400

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

Valentin Samir's avatar
Valentin Samir committed
401
    #: The current :class:`models.User<cas_server.models.User>` object
Valentin Samir's avatar
Valentin Samir committed
402
    user = None
Valentin Samir's avatar
Valentin Samir committed
403
    #: The form to display to the user
Valentin Samir's avatar
Valentin Samir committed
404
    form = None
405

Valentin Samir's avatar
Valentin Samir committed
406
    #: current :class:`django.http.HttpRequest` object
407
    request = None
Valentin Samir's avatar
Valentin Samir committed
408
    #: service GET/POST parameter
409
    service = None
Valentin Samir's avatar
Valentin Samir committed
410
    #: ``True`` if renew GET/POST parameter is present and not "False"
411
    renew = None
Valentin Samir's avatar
Valentin Samir committed
412 413 414
    #: the warn GET/POST parameter
    warn = None
    #: the gateway GET/POST parameter
415
    gateway = None
Valentin Samir's avatar
Valentin Samir committed
416
    #: the method GET/POST parameter
417
    method = None
Valentin Samir's avatar
Valentin Samir committed
418 419 420

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

Valentin Samir's avatar
Valentin Samir committed
423
    #: ``True`` if the user has just authenticated
Valentin Samir's avatar
Valentin Samir committed
424
    renewed = False
Valentin Samir's avatar
Valentin Samir committed
425
    #: ``True`` if renew GET/POST parameter is present and not "False"
426
    warned = False
427

Valentin Samir's avatar
Valentin Samir committed
428 429
    #: The :class:`FederateAuth` transmited username (only used if ``settings.CAS_FEDERATE``
    #: is ``True``)
430
    username = None
Valentin Samir's avatar
Valentin Samir committed
431 432
    #: The :class:`FederateAuth` transmited ticket (only used if ``settings.CAS_FEDERATE`` is
    #: ``True``)
433
    ticket = None
434

Valentin Samir's avatar
Valentin Samir committed
435 436 437 438 439 440 441 442
    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
443 444 445 446 447
        """
            Initialize POST received parameters

            :param django.http.HttpRequest request: The current request object
        """
448 449
        self.request = request
        self.service = request.POST.get('service')
Valentin Samir's avatar
style  
Valentin Samir committed
450
        self.renew = bool(request.POST.get('renew') and request.POST['renew'] != "False")
451 452
        self.gateway = request.POST.get('gateway')
        self.method = request.POST.get('method')
453
        self.ajax = settings.CAS_ENABLE_AJAX_AUTH and 'HTTP_X_AJAX' in request.META
454 455
        if request.POST.get('warned') and request.POST['warned'] != "False":
            self.warned = True
Valentin Samir's avatar
Valentin Samir committed
456 457 458
        self.warn = request.POST.get('warn')
        if settings.CAS_FEDERATE:
            self.username = request.POST.get('username')
Valentin Samir's avatar
Valentin Samir committed
459 460
            # in federated mode, the valdated indentity provider CAS ticket is used as password
            self.ticket = request.POST.get('password')
461

462 463 464 465 466 467
    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
468
    def check_lt(self):
Valentin Samir's avatar
Valentin Samir committed
469 470 471 472 473 474
        """
            Check is the POSTed LoginTicket is valid, if yes invalide it

            :return: ``True`` if the LoginTicket is valid, ``False`` otherwise
            :rtype: bool
        """
475
        # save LT for later check
476
        lt_valid = self.request.session.get('lt', [])
Valentin Samir's avatar
Valentin Samir committed
477
        lt_send = self.request.POST.get('lt')
478
        # generate a new LT (by posting the LT has been consumed)
479
        self.gen_lt()
480
        # check if send LT is valid
481
        if lt_send not in lt_valid:
Valentin Samir's avatar
Valentin Samir committed
482 483
            return False
        else:
484
            self.request.session['lt'].remove(lt_send)
Valentin Samir's avatar
Valentin Samir committed
485 486
            # 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
487
            self.request.session['lt'] = self.request.session['lt']
Valentin Samir's avatar
Valentin Samir committed
488 489 490
            return True

    def post(self, request, *args, **kwargs):
Valentin Samir's avatar
Valentin Samir committed
491
        """
492
            method called on POST request on this view
Valentin Samir's avatar
Valentin Samir committed
493 494 495 496

            :param django.http.HttpRequest request: The current request object
        """
        # initialize class parameters
Valentin Samir's avatar
Valentin Samir committed
497
        self.init_post(request)
Valentin Samir's avatar
Valentin Samir committed
498
        # process the POST request
Valentin Samir's avatar
Valentin Samir committed
499 500
        ret = self.process_post()
        if ret == self.INVALID_LOGIN_TICKET:
501 502 503
            messages.add_message(
                self.request,
                messages.ERROR,
504
                _(u"Invalid login ticket, please try to log in again")
Valentin Samir's avatar
Valentin Samir committed
505
            )
Valentin Samir's avatar
Valentin Samir committed
506
        elif ret == self.USER_LOGIN_OK:
Valentin Samir's avatar
Valentin Samir committed
507 508
            # On successful login, update the :class:`models.User<cas_server.models.User>` ``date``
            # attribute by saving it. (``auto_now=True``)
509 510 511 512
            self.user = models.User.objects.get_or_create(
                username=self.request.session['username'],
                session_key=self.request.session.session_key
            )[0]
513
            self.user.last_login = timezone.now()
514
            self.user.save()
Valentin Samir's avatar
Valentin Samir committed
515
        elif ret == self.USER_LOGIN_FAILURE:  # bad user login
516 517
            if settings.CAS_FEDERATE:
                self.ticket = None
518
                self.username = None
519
                self.init_form()
520 521
            # preserve valid LoginTickets from session flush
            lt = self.request.session.get('lt', [])
Valentin Samir's avatar
Valentin Samir committed
522
            # On login failure, flush the session
Valentin Samir's avatar
Valentin Samir committed
523
            self.logout()
524 525
            # restore valid LoginTickets
            self.request.session['lt'] = lt
Valentin Samir's avatar
Valentin Samir committed
526 527
        elif ret == self.USER_ALREADY_LOGGED:
            pass
Valentin Samir's avatar
Valentin Samir committed
528 529 530
        else:  # pragma: no cover (should no happen)
            raise EnvironmentError("invalid output for LoginView.process_post")
        # call the GET/POST common part
531 532 533 534 535 536 537 538 539 540 541
        response = self.common()
        if self.warn:
            utils.set_cookie(
                response,
                "warn",
                "on",
                10 * 365 * 24 * 3600
            )
        else:
            response.delete_cookie("warn")
        return response
Valentin Samir's avatar
Valentin Samir committed
542

Valentin Samir's avatar
style  
Valentin Samir committed
543
    def process_post(self):
544 545
        """
            Analyse the POST request:
Valentin Samir's avatar
Valentin Samir committed
546

547 548
                * check that the LoginTicket is valid
                * check that the user sumited credentials are valid
Valentin Samir's avatar
Valentin Samir committed
549 550 551 552 553 554 555 556 557 558

            :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
559
        """
Valentin Samir's avatar
Valentin Samir committed
560
        if not self.check_lt():
561
            self.init_form(self.request.POST)
562
            logger.warning("Received an invalid login ticket")
Valentin Samir's avatar
Valentin Samir committed
563 564
            return self.INVALID_LOGIN_TICKET
        elif not self.request.session.get("authenticated") or self.renew:
Valentin Samir's avatar
Valentin Samir committed
565
            # authentication request receive, initialize the form to use
Valentin Samir's avatar
Valentin Samir committed
566 567 568 569 570 571
            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
572 573
                self.renewed = True
                self.warned = True
Valentin Samir's avatar
Valentin Samir committed
574
                logger.info("User %s successfully authenticated" % self.request.session["username"])
Valentin Samir's avatar
Valentin Samir committed
575
                return self.USER_LOGIN_OK
Valentin Samir's avatar
Valentin Samir committed
576
            else:
577
                logger.warning("A login attempt failed")
Valentin Samir's avatar
Valentin Samir committed
578 579
                return self.USER_LOGIN_FAILURE
        else:
580
            logger.warning("Received a login attempt for an already-active user")
Valentin Samir's avatar
Valentin Samir committed
581
            return self.USER_ALREADY_LOGGED
582

Valentin Samir's avatar
Valentin Samir committed
583
    def init_get(self, request):
Valentin Samir's avatar
Valentin Samir committed
584 585 586 587 588
        """
            Initialize GET received parameters

            :param django.http.HttpRequest request: The current request object
        """
589 590
        self.request = request
        self.service = request.GET.get('service')
Valentin Samir's avatar
style  
Valentin Samir committed
591
        self.renew = bool(request.GET.get('renew') and request.GET['renew'] != "False")
592 593
        self.gateway = request.GET.get('gateway')
        self.method = request.GET.get('method')
594
        self.ajax = settings.CAS_ENABLE_AJAX_AUTH and 'HTTP_X_AJAX' in request.META
Valentin Samir's avatar
Valentin Samir committed
595 596
        self.warn = request.GET.get('warn')
        if settings.CAS_FEDERATE:
Valentin Samir's avatar
Valentin Samir committed
597 598
            # here username and ticket are fetch from the session after a redirection from
            # FederateAuth.get
599 600 601 602 603 604
            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"]
605

Valentin Samir's avatar
Valentin Samir committed
606
    def get(self, request, *args, **kwargs):
Valentin Samir's avatar
Valentin Samir committed
607
        """
608
            method called on GET request on this view
Valentin Samir's avatar
Valentin Samir committed
609 610 611 612

            :param django.http.HttpRequest request: The current request object
        """
        # initialize class parameters
Valentin Samir's avatar
Valentin Samir committed
613
        self.init_get(request)
Valentin Samir's avatar
Valentin Samir committed
614
        # process the GET request
Valentin Samir's avatar
Valentin Samir committed
615
        self.process_get()
Valentin Samir's avatar
Valentin Samir committed
616
        # call the GET/POST common part
Valentin Samir's avatar
Valentin Samir committed
617 618 619
        return self.common()

    def process_get(self):
Valentin Samir's avatar
Valentin Samir committed
620 621 622 623 624 625 626 627 628 629
        """
            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
        """
630 631
        # generate a new LT
        self.gen_lt()
Valentin Samir's avatar
Valentin Samir committed
632
        if not self.request.session.get("authenticated") or self.renew:
Valentin Samir's avatar
Valentin Samir committed
633
            # authentication will be needed, initialize the form to use
634
            self.init_form()
Valentin Samir's avatar
Valentin Samir committed
635 636
            return self.USER_NOT_AUTHENTICATED
        return self.USER_AUTHENTICATED
Valentin Samir's avatar
Valentin Samir committed
637

638
    def init_form(self, values=None):
Valentin Samir's avatar
Valentin Samir committed
639 640 641 642 643
        """
            Initialization of the good form depending of POST and GET parameters

            :param django.http.QueryDict values: A POST or GET QueryDict
        """
644 645 646
        if values:
            values = values.copy()
            values['lt'] = self.request.session['lt'][-1]
Valentin Samir's avatar
Valentin Samir committed
647 648 649
        form_initial = {
            'service': self.service,
            'method': self.method,
650 651 652
            'warn': (
                self.warn or self.request.session.get("warn") or self.request.COOKIES.get('warn')
            ),
653 654
            'lt': self.request.session['lt'][-1],
            'renew': self.renew
Valentin Samir's avatar
Valentin Samir committed
655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671
        }
        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
            )
672

673
    def service_login(self):
Valentin Samir's avatar
Valentin Samir committed
674
        """
675
            Perform login against a service
Valentin Samir's avatar
Valentin Samir committed
676 677 678 679 680 681 682 683 684 685 686

            :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
        """
687 688
        try:
            # is the service allowed
Valentin Samir's avatar
Valentin Samir committed
689
            service_pattern = ServicePattern.validate(self.service)
690 691 692 693
            # 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
694
                messages.add_message(
695 696
                    self.request,
                    messages.WARNING,
Valentin Samir's avatar
PEP8  
Valentin Samir committed
697 698
                    _(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
699
                )
700 701
                if self.ajax:
                    data = {"status": "error", "detail": "confirmation needed"}
Valentin Samir's avatar
style  
Valentin Samir committed
702
                    return json_response(self.request, data)
703
                else:
704 705 706 707 708 709 710 711
                    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]
                    })
712 713 714
                    return render(
                        self.request,
                        settings.CAS_WARN_TEMPLATE,
715
                        utils.context({'form': warn_form})
Valentin Samir's avatar
Valentin Samir committed
716
                    )
717 718
            else:
                # redirect, using method ?
Valentin Samir's avatar
PEP8  
Valentin Samir committed
719
                list(messages.get_messages(self.request))  # clean messages before leaving django
720 721 722
                redirect_url = self.user.get_service_url(
                    self.service,
                    service_pattern,
723
                    renew=self.renewed
Valentin Samir's avatar
Valentin Samir committed
724
                )
725 726 727 728
                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
729
                    return json_response(self.request, data)
Valentin Samir's avatar
Valentin Samir committed
730
        except ServicePattern.DoesNotExist:
731
            error = 1
732 733 734
            messages.add_message(
                self.request,
                messages.ERROR,
735
                _(u'Service %(url)s not allowed.') % {'url': self.service}
736 737
            )
        except models.BadUsername:
738
            error = 2
739 740 741
            messages.add_message(
                self.request,
                messages.ERROR,
742
                _(u"Username not allowed")
743 744
            )
        except models.BadFilter:
745
            error = 3
746 747 748
            messages.add_message(
                self.request,
                messages.ERROR,
749
                _(u"User characteristics not allowed")
750 751
            )
        except models.UserFieldNotDefined:
752
            error = 4
753 754 755
            messages.add_message(
                self.request,
                messages.ERROR,
756
                _(u"The attribute %(field)s is needed to use"
Valentin Samir's avatar
PEP8  
Valentin Samir committed
757
                  u" that service") % {'field': service_pattern.user_field}
758 759 760
            )

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

765 766 767 768
        if not self.ajax:
            return render(
                self.request,
                settings.CAS_LOGGED_TEMPLATE,
769
                utils.context({'session': self.request.session})
770 771 772
            )
        else:
            data = {"status": "error", "detail": "auth", "code": error}
Valentin Samir's avatar
style  
Valentin Samir committed
773
            return json_response(self.request, data)
Valentin Samir's avatar
Valentin Samir committed
774

775
    def authenticated(self):
Valentin Samir's avatar
Valentin Samir committed
776 777 778 779 780 781 782 783 784 785
        """
            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
786
        try:
Valentin Samir's avatar
Valentin Samir committed
787 788
            self.user = models.User.objects.get(
                username=self.request.session.get("username"),
Valentin Samir's avatar
oops  
Valentin Samir committed
789
                session_key=self.request.session.session_key
Valentin Samir's avatar
Valentin Samir committed
790
            )
Valentin Samir's avatar
Valentin Samir committed
791
        # if not found, flush the session and redirect to the login page
792
        except models.User.DoesNotExist:
Valentin Samir's avatar
Valentin Samir committed
793 794 795 796 797
            logger.warning(
                "User %s seems authenticated but is not found in the database." % (
                    self.request.session.get("username"),
                )
            )
798
            self.logout()
799 800 801 802 803 804
            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
805
                return json_response(self.request, data)
806 807
            else:
                return utils.redirect_params("cas_server:login", params=self.request.GET)
808

809
        # if login against a service
810 811
        if self.service:
            return self.service_login()
Valentin Samir's avatar
Valentin Samir committed
812
        # else display the logged template
813
        else:
814 815
            if self.ajax:
                data = {"status": "success", "detail": "logged"}
Valentin Samir's avatar
style  
Valentin Samir committed
816
                return json_response(self.request, data)
817 818 819 820
            else:
                return render(
                    self.request,
                    settings.CAS_LOGGED_TEMPLATE,
821
                    utils.context({'session': self.request.session})
822
                )
823 824

    def not_authenticated(self):
Valentin Samir's avatar
Valentin Samir committed
825 826 827 828 829 830 831 832 833 834
        """
            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
        """
835
        if self.service:
Valentin Samir's avatar
Valentin Samir committed
836
            try:
Valentin Samir's avatar
Valentin Samir committed
837
                service_pattern = ServicePattern.validate(self.service)
838
                if self.gateway and not self.ajax:
Valentin Samir's avatar
PEP8  
Valentin Samir committed
839 840
                    # clean messages before leaving django
                    list(messages.get_messages(self.request))
841
                    return HttpResponseRedirect(self.service)
842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859

                if settings.CAS_SHOW_SERVICE_MESSAGES:
                    if self.request.session.get("authenticated") and self.renew:
                        messages.add_message(
                            self.request,
                            messages.WARNING,
                            _(u"Authentication renewal required by service %(name)s (%(url)s).") %
                            {'name': service_pattern.name, 'url': self.service}
                        )
                    else:
                        messages.add_message(
                            self.request,
                            messages.WARNING,
                            _(u"Authentication required by service %(name)s (%(url)s).") %
                            {'name': service_pattern.name, 'url': self.service}
                        )
            except ServicePattern.DoesNotExist:
                if settings.CAS_SHOW_SERVICE_MESSAGES:
Valentin Samir's avatar
Valentin Samir committed
860
                    messages.add_message(
861
                        self.request,
862 863
                        messages.ERROR,
                        _(u'Service %s not allowed') % self.service
Valentin Samir's avatar
Valentin Samir committed
864
                    )
865 866 867 868 869 870
        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
871
            return json_response(self.request, data)
872
        else:
Valentin Samir's avatar
Valentin Samir committed
873 874 875 876 877
            if settings.CAS_FEDERATE:
                if self.username and self.ticket:
                    return render(
                        self.request,
                        settings.CAS_LOGIN_TEMPLATE,
878
                        utils.context({
Valentin Samir's avatar
Valentin Samir committed
879 880 881
                            'form': self.form,
                            'auto_submit': True,
                            'post_url': reverse("cas_server:login")
882
                        })
Valentin Samir's avatar
Valentin Samir committed
883 884 885
                    )
                else:
                    if (
886
                        self.request.COOKIES.get('remember_provider') and
887
                        FederatedIendityProvider.objects.filter(
888
                            suffix=self.request.COOKIES['remember_provider']
889
                        )
Valentin Samir's avatar
Valentin Samir committed
890 891 892 893 894
                    ):
                        params = utils.copy_params(self.request.GET)
                        url = utils.reverse_params(
                            "cas_server:federateAuth",
                            params=params,
895
                            kwargs=dict(provider=self.request.COOKIES['remember_provider'])
Valentin Samir's avatar
Valentin Samir committed
896 897 898
                        )
                        return HttpResponseRedirect(url)
                    else:
899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916
                        # if user is authenticated and auth renewal is requested, redirect directly
                        # to the user identity provider
                        if self.renew and self.request.session.get("authenticated"):
                            try:
                                user = FederatedUser.get_from_federated_username(
                                    self.request.session.get("username")
                                )
                                params = utils.copy_params(self.request.GET)
                                url = utils.reverse_params(
                                    "cas_server:federateAuth",
                                    params=params,
                                    kwargs=dict(provider=user.provider.suffix)
                                )
                                return HttpResponseRedirect(url)
                            # Should normally not happen: if the user is logged, it exists in the
                            # database.
                            except FederatedUser.DoesNotExist:  # pragma: no cover
                                pass
Valentin Samir's avatar
Valentin Samir committed
917 918
                        return render(
                            self.request,
919 920 921 922 923
                            settings.CAS_LOGIN_TEMPLATE,
                            utils.context({
                                'form': self.form,
                                'post_url': reverse("cas_server:federateAuth")
                            })
Valentin Samir's avatar
Valentin Samir committed
924 925
                        )
            else:
926 927 928 929 930
                return render(
                    self.request,
                    settings.CAS_LOGIN_TEMPLATE,
                    utils.context({'form': self.form})
                )
Valentin Samir's avatar
Valentin Samir committed
931

932
    def common(self):
Valentin Samir's avatar
Valentin Samir committed
933 934 935 936 937 938 939 940 941
        """
            Common part execute uppon GET and POST request

            :return:
                * The returned value of :meth:`authenticated` if the user is authenticated and
                  not requesting for authentication or if the authentication has just been renewed
                * The returned value of :meth:`not_authenticated` otherwise
            :rtype: django.http.HttpResponse
        """
942 943 944 945 946
        # if authenticated and successfully renewed authentication if needed
        if self.request.session.get("authenticated") and (not self.renew or self.renewed):
            return self.authenticated()
        else:
            return self.not_authenticated()
Valentin Samir's avatar
Valentin Samir committed
947

Valentin Samir's avatar
PEP8  
Valentin Samir committed
948

949 950 951
class Auth(CsrfExemptView):
    """
        A simple view to validate username/password/service tuple
Valentin Samir's avatar
Valentin Samir committed
952

953 954 955
        csrf is disable as it is intended to be used by programs. Security is assured by a shared
        secret between the programs dans django-cas-server.
    """
956 957 958

    @staticmethod
    def post(request):
Valentin Samir's avatar
Valentin Samir committed
959
        """