views.py 58.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
            if auth is not None:
150
                params = utils.copy_params(request.GET, ignore={"forget_provider"})
151
                url = auth.get_logout_url()
152 153 154 155
                response = HttpResponseRedirect(utils.update_url(url, params))
                if request.GET.get("forget_provider"):
                    response.delete_cookie("remember_provider")
                return response
156 157
        # if service is set, redirect to service after logout
        if self.service:
Valentin Samir's avatar
PEP8  
Valentin Samir committed
158
            list(messages.get_messages(request))  # clean messages before leaving the django app
159
            return HttpResponseRedirect(self.service)
Valentin Samir's avatar
Valentin Samir committed
160
        # if service is not set but url is set, redirect to url after logout
Valentin Samir's avatar
Valentin Samir committed
161
        elif self.url:
Valentin Samir's avatar
PEP8  
Valentin Samir committed
162
            list(messages.get_messages(request))  # clean messages before leaving the django app
Valentin Samir's avatar
Valentin Samir committed
163
            return HttpResponseRedirect(self.url)
164
        else:
Valentin Samir's avatar
Valentin Samir committed
165
            # build logout message depending of the number of sessions the user logs out
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
            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
186 187
            # 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.
188
            if settings.CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT:
189
                messages.add_message(request, messages.SUCCESS, logout_msg)
190 191
                if self.ajax:
                    url = reverse("cas_server:login")
192 193 194 195 196 197
                    data = {
                        'status': 'success',
                        'detail': 'logout',
                        'url': url,
                        'session_nb': session_nb
                    }
Valentin Samir's avatar
style  
Valentin Samir committed
198
                    return json_response(request, data)
199 200
                else:
                    return redirect("cas_server:login")
201
            else:
202
                if self.ajax:
203
                    data = {'status': 'success', 'detail': 'logout', 'session_nb': session_nb}
Valentin Samir's avatar
style  
Valentin Samir committed
204
                    return json_response(request, data)
205
                else:
206 207 208
                    return render(
                        request,
                        settings.CAS_LOGOUT_TEMPLATE,
209
                        utils.context({'logout_msg': logout_msg})
210
                    )
211

Valentin Samir's avatar
PEP8  
Valentin Samir committed
212

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

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

224
    def get_cas_client(self, request, provider):
Valentin Samir's avatar
Valentin Samir committed
225 226 227 228 229 230
        """
            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
231 232
            :rtype: :class:`federate.CASFederateValidateUser
                <cas_server.federate.CASFederateValidateUser>`
Valentin Samir's avatar
Valentin Samir committed
233 234
        """
        # compute the current url, ignoring ticket dans provider GET parameters
235
        service_url = utils.get_current_url(request, {"ticket", "provider"})
236
        self.service_url = service_url
237
        return CASFederateValidateUser(provider, service_url)
238

Valentin Samir's avatar
Valentin Samir committed
239
    def post(self, request, provider=None):
Valentin Samir's avatar
Valentin Samir committed
240 241 242 243 244 245 246
        """
            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
247
        if not settings.CAS_FEDERATE:
248
            logger.warning("CAS_FEDERATE is False, set it to True to use the federated mode")
249
            return redirect("cas_server:login")
Valentin Samir's avatar
Valentin Samir committed
250 251
        # POST with a provider suffix, this is probably an SLO request. csrf is disabled for
        # allowing SLO requests reception
252 253
        try:
            provider = FederatedIendityProvider.objects.get(suffix=provider)
254 255 256
            auth = self.get_cas_client(request, provider)
            try:
                auth.clean_sessions(request.POST['logoutRequest'])
257
            except (KeyError, AttributeError):
258 259 260
                pass
            return HttpResponse("ok")
        # else, a User is trying to log in using an identity provider
261
        except FederatedIendityProvider.DoesNotExist:
262 263
            # Manually checking for csrf to protect the code below
            reason = CsrfViewMiddleware().process_view(request, None, (), {})
264
            if reason is not None:  # pragma: no cover (csrf checks are disabled during tests)
265 266 267 268 269
                return reason  # Failed the test, stop here.
            form = forms.FederateSelect(request.POST)
            if form.is_valid():
                params = utils.copy_params(
                    request.POST,
270
                    ignore={"provider", "csrfmiddlewaretoken", "ticket", "lt"}
271
                )
Valentin Samir's avatar
Valentin Samir committed
272 273
                if params.get("renew") == "False":
                    del params["renew"]
274 275
                url = utils.reverse_params(
                    "cas_server:federateAuth",
276
                    kwargs=dict(provider=form.cleaned_data["provider"].suffix),
277 278
                    params=params
                )
279
                return HttpResponseRedirect(url)
280 281
            else:
                return redirect("cas_server:login")
Valentin Samir's avatar
Valentin Samir committed
282 283

    def get(self, request, provider=None):
Valentin Samir's avatar
Valentin Samir committed
284 285 286 287 288 289 290
        """
            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
291
        if not settings.CAS_FEDERATE:
292 293
            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
294 295
        # Is the user is already authenticated, no need to request authentication to the user
        # identity provider.
296 297
        if self.request.session.get("authenticated"):
            logger.warning("User already authenticated, dropping federate authentication request")
298
            return redirect("cas_server:login")
299
        try:
Valentin Samir's avatar
Valentin Samir committed
300
            # get the identity provider from its suffix
301
            provider = FederatedIendityProvider.objects.get(suffix=provider)
Valentin Samir's avatar
Valentin Samir committed
302
            # get a CAS client for the user identity provider
303
            auth = self.get_cas_client(request, provider)
Valentin Samir's avatar
Valentin Samir committed
304
            # if no ticket submited, redirect to the identity provider CAS login page
305
            if 'ticket' not in request.GET:
306
                logger.info("Trying to authenticate again %s" % auth.provider.server_url)
Valentin Samir's avatar
Valentin Samir committed
307
                return HttpResponseRedirect(auth.get_login_url())
308 309
            else:
                ticket = request.GET['ticket']
310 311 312 313 314 315 316 317
                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
                            )
318
                        )
319
                        params = utils.copy_params(request.GET, ignore={"ticket", "remember"})
320 321 322 323 324 325
                        request.session["federate_username"] = auth.federated_username
                        request.session["federate_ticket"] = ticket
                        auth.register_slo(
                            auth.federated_username,
                            request.session.session_key,
                            ticket
326
                        )
327 328 329
                        # 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)
330 331 332 333 334 335 336
                        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,
337
                                "remember_provider",
338 339 340 341
                                provider.suffix,
                                max_age
                            )
                        return response
342 343 344
                    # else redirect to the identity provider CAS login page
                    else:
                        logger.info(
345 346 347 348 349 350 351
                            (
                                "Got a invalid ticket %s from %s for service %s. "
                                "Retrying to authenticate"
                            ) % (
                                ticket,
                                auth.provider.server_url,
                                self.service_url
352 353 354 355 356 357 358 359 360 361
                            )
                        )
                        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 "
362 363
                            u"ticket %(ticket)s validation: %(error)r"
                        ) % {'ticket': ticket, 'error': error}
364
                    )
365
                    response = redirect("cas_server:login")
366
                    response.delete_cookie("remember_provider")
367
                    return response
368
        except FederatedIendityProvider.DoesNotExist:
369
            logger.warning("Identity provider suffix %s not found" % provider)
Valentin Samir's avatar
Valentin Samir committed
370
            # if the identity provider is not found, redirect to the login page
371
            return redirect("cas_server:login")
Valentin Samir's avatar
Valentin Samir committed
372 373


Valentin Samir's avatar
Valentin Samir committed
374
class LoginView(View, LogoutMixin):
Valentin Samir's avatar
Valentin Samir committed
375
    """credential requestor / acceptor"""
376 377 378 379

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

Valentin Samir's avatar
Valentin Samir committed
380
    #: The current :class:`models.User<cas_server.models.User>` object
Valentin Samir's avatar
Valentin Samir committed
381
    user = None
Valentin Samir's avatar
Valentin Samir committed
382
    #: The form to display to the user
Valentin Samir's avatar
Valentin Samir committed
383
    form = None
384

Valentin Samir's avatar
Valentin Samir committed
385
    #: current :class:`django.http.HttpRequest` object
386
    request = None
Valentin Samir's avatar
Valentin Samir committed
387
    #: service GET/POST parameter
388
    service = None
Valentin Samir's avatar
Valentin Samir committed
389
    #: ``True`` if renew GET/POST parameter is present and not "False"
390
    renew = None
Valentin Samir's avatar
Valentin Samir committed
391 392 393
    #: the warn GET/POST parameter
    warn = None
    #: the gateway GET/POST parameter
394
    gateway = None
Valentin Samir's avatar
Valentin Samir committed
395
    #: the method GET/POST parameter
396
    method = None
Valentin Samir's avatar
Valentin Samir committed
397 398 399

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

Valentin Samir's avatar
Valentin Samir committed
402
    #: ``True`` if the user has just authenticated
Valentin Samir's avatar
Valentin Samir committed
403
    renewed = False
Valentin Samir's avatar
Valentin Samir committed
404
    #: ``True`` if renew GET/POST parameter is present and not "False"
405
    warned = False
406

Valentin Samir's avatar
Valentin Samir committed
407 408
    #: The :class:`FederateAuth` transmited username (only used if ``settings.CAS_FEDERATE``
    #: is ``True``)
409
    username = None
Valentin Samir's avatar
Valentin Samir committed
410 411
    #: The :class:`FederateAuth` transmited ticket (only used if ``settings.CAS_FEDERATE`` is
    #: ``True``)
412
    ticket = None
413

Valentin Samir's avatar
Valentin Samir committed
414 415 416 417 418 419 420 421
    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
422 423 424 425 426
        """
            Initialize POST received parameters

            :param django.http.HttpRequest request: The current request object
        """
427 428
        self.request = request
        self.service = request.POST.get('service')
Valentin Samir's avatar
style  
Valentin Samir committed
429
        self.renew = bool(request.POST.get('renew') and request.POST['renew'] != "False")
430 431
        self.gateway = request.POST.get('gateway')
        self.method = request.POST.get('method')
432
        self.ajax = settings.CAS_ENABLE_AJAX_AUTH and 'HTTP_X_AJAX' in request.META
433 434
        if request.POST.get('warned') and request.POST['warned'] != "False":
            self.warned = True
Valentin Samir's avatar
Valentin Samir committed
435 436 437
        self.warn = request.POST.get('warn')
        if settings.CAS_FEDERATE:
            self.username = request.POST.get('username')
Valentin Samir's avatar
Valentin Samir committed
438 439
            # in federated mode, the valdated indentity provider CAS ticket is used as password
            self.ticket = request.POST.get('password')
440

441 442 443 444 445 446
    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
447
    def check_lt(self):
Valentin Samir's avatar
Valentin Samir committed
448 449 450 451 452 453
        """
            Check is the POSTed LoginTicket is valid, if yes invalide it

            :return: ``True`` if the LoginTicket is valid, ``False`` otherwise
            :rtype: bool
        """
454
        # save LT for later check
455
        lt_valid = self.request.session.get('lt', [])
Valentin Samir's avatar
Valentin Samir committed
456
        lt_send = self.request.POST.get('lt')
457
        # generate a new LT (by posting the LT has been consumed)
458
        self.gen_lt()
459
        # check if send LT is valid
460
        if lt_send not in lt_valid:
Valentin Samir's avatar
Valentin Samir committed
461 462
            return False
        else:
463
            self.request.session['lt'].remove(lt_send)
Valentin Samir's avatar
Valentin Samir committed
464 465
            # 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
466
            self.request.session['lt'] = self.request.session['lt']
Valentin Samir's avatar
Valentin Samir committed
467 468 469
            return True

    def post(self, request, *args, **kwargs):
Valentin Samir's avatar
Valentin Samir committed
470 471 472 473 474 475
        """
            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
476
        self.init_post(request)
Valentin Samir's avatar
Valentin Samir committed
477
        # process the POST request
Valentin Samir's avatar
Valentin Samir committed
478 479
        ret = self.process_post()
        if ret == self.INVALID_LOGIN_TICKET:
480 481 482
            messages.add_message(
                self.request,
                messages.ERROR,
483
                _(u"Invalid login ticket, please retry to login")
Valentin Samir's avatar
Valentin Samir committed
484
            )
Valentin Samir's avatar
Valentin Samir committed
485
        elif ret == self.USER_LOGIN_OK:
Valentin Samir's avatar
Valentin Samir committed
486 487
            # On successful login, update the :class:`models.User<cas_server.models.User>` ``date``
            # attribute by saving it. (``auto_now=True``)
488 489 490 491 492
            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
493
        elif ret == self.USER_LOGIN_FAILURE:  # bad user login
494 495
            if settings.CAS_FEDERATE:
                self.ticket = None
496
                self.username = None
497
                self.init_form()
498 499
            # preserve valid LoginTickets from session flush
            lt = self.request.session.get('lt', [])
Valentin Samir's avatar
Valentin Samir committed
500
            # On login failure, flush the session
Valentin Samir's avatar
Valentin Samir committed
501
            self.logout()
502 503
            # restore valid LoginTickets
            self.request.session['lt'] = lt
Valentin Samir's avatar
Valentin Samir committed
504 505
        elif ret == self.USER_ALREADY_LOGGED:
            pass
Valentin Samir's avatar
Valentin Samir committed
506 507 508
        else:  # pragma: no cover (should no happen)
            raise EnvironmentError("invalid output for LoginView.process_post")
        # call the GET/POST common part
509 510 511 512 513 514 515 516 517 518 519
        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
520

Valentin Samir's avatar
style  
Valentin Samir committed
521
    def process_post(self):
522 523
        """
            Analyse the POST request:
Valentin Samir's avatar
Valentin Samir committed
524

525 526
                * check that the LoginTicket is valid
                * check that the user sumited credentials are valid
Valentin Samir's avatar
Valentin Samir committed
527 528 529 530 531 532 533 534 535 536

            :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
537
        """
Valentin Samir's avatar
Valentin Samir committed
538
        if not self.check_lt():
539
            self.init_form(self.request.POST)
Valentin Samir's avatar
Valentin Samir committed
540
            logger.warning("Receive an invalid login ticket")
Valentin Samir's avatar
Valentin Samir committed
541 542
            return self.INVALID_LOGIN_TICKET
        elif not self.request.session.get("authenticated") or self.renew:
Valentin Samir's avatar
Valentin Samir committed
543
            # authentication request receive, initialize the form to use
Valentin Samir's avatar
Valentin Samir committed
544 545 546 547 548 549
            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
550 551
                self.renewed = True
                self.warned = True
Valentin Samir's avatar
Valentin Samir committed
552
                logger.info("User %s successfully authenticated" % self.request.session["username"])
Valentin Samir's avatar
Valentin Samir committed
553
                return self.USER_LOGIN_OK
Valentin Samir's avatar
Valentin Samir committed
554
            else:
Valentin Samir's avatar
Valentin Samir committed
555
                logger.warning("A logging attemps failed")
Valentin Samir's avatar
Valentin Samir committed
556 557
                return self.USER_LOGIN_FAILURE
        else:
Valentin Samir's avatar
Valentin Samir committed
558
            logger.warning("Receuve a logging attempt whereas the user is already logged")
Valentin Samir's avatar
Valentin Samir committed
559
            return self.USER_ALREADY_LOGGED
560

Valentin Samir's avatar
Valentin Samir committed
561
    def init_get(self, request):
Valentin Samir's avatar
Valentin Samir committed
562 563 564 565 566
        """
            Initialize GET received parameters

            :param django.http.HttpRequest request: The current request object
        """
567 568
        self.request = request
        self.service = request.GET.get('service')
Valentin Samir's avatar
style  
Valentin Samir committed
569
        self.renew = bool(request.GET.get('renew') and request.GET['renew'] != "False")
570 571
        self.gateway = request.GET.get('gateway')
        self.method = request.GET.get('method')
572
        self.ajax = settings.CAS_ENABLE_AJAX_AUTH and 'HTTP_X_AJAX' in request.META
Valentin Samir's avatar
Valentin Samir committed
573 574
        self.warn = request.GET.get('warn')
        if settings.CAS_FEDERATE:
Valentin Samir's avatar
Valentin Samir committed
575 576
            # here username and ticket are fetch from the session after a redirection from
            # FederateAuth.get
577 578 579 580 581 582
            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"]
583

Valentin Samir's avatar
Valentin Samir committed
584
    def get(self, request, *args, **kwargs):
Valentin Samir's avatar
Valentin Samir committed
585 586 587 588 589 590
        """
            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
591
        self.init_get(request)
Valentin Samir's avatar
Valentin Samir committed
592
        # process the GET request
Valentin Samir's avatar
Valentin Samir committed
593
        self.process_get()
Valentin Samir's avatar
Valentin Samir committed
594
        # call the GET/POST common part
Valentin Samir's avatar
Valentin Samir committed
595 596 597
        return self.common()

    def process_get(self):
Valentin Samir's avatar
Valentin Samir committed
598 599 600 601 602 603 604 605 606 607
        """
            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
        """
608 609
        # generate a new LT
        self.gen_lt()
Valentin Samir's avatar
Valentin Samir committed
610
        if not self.request.session.get("authenticated") or self.renew:
Valentin Samir's avatar
Valentin Samir committed
611
            # authentication will be needed, initialize the form to use
612
            self.init_form()
Valentin Samir's avatar
Valentin Samir committed
613 614
            return self.USER_NOT_AUTHENTICATED
        return self.USER_AUTHENTICATED
Valentin Samir's avatar
Valentin Samir committed
615

616
    def init_form(self, values=None):
Valentin Samir's avatar
Valentin Samir committed
617 618 619 620 621
        """
            Initialization of the good form depending of POST and GET parameters

            :param django.http.QueryDict values: A POST or GET QueryDict
        """
622 623 624
        if values:
            values = values.copy()
            values['lt'] = self.request.session['lt'][-1]
Valentin Samir's avatar
Valentin Samir committed
625 626 627
        form_initial = {
            'service': self.service,
            'method': self.method,
628 629 630
            'warn': (
                self.warn or self.request.session.get("warn") or self.request.COOKIES.get('warn')
            ),
631 632
            'lt': self.request.session['lt'][-1],
            'renew': self.renew
Valentin Samir's avatar
Valentin Samir committed
633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649
        }
        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
            )
650

651
    def service_login(self):
Valentin Samir's avatar
Valentin Samir committed
652 653 654 655 656 657 658 659 660 661 662 663 664
        """
            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
        """
665 666
        try:
            # is the service allowed
Valentin Samir's avatar
Valentin Samir committed
667
            service_pattern = ServicePattern.validate(self.service)
668 669 670 671
            # 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
672
                messages.add_message(
673 674
                    self.request,
                    messages.WARNING,
Valentin Samir's avatar
PEP8  
Valentin Samir committed
675 676
                    _(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
677
                )
678 679
                if self.ajax:
                    data = {"status": "error", "detail": "confirmation needed"}
Valentin Samir's avatar
style  
Valentin Samir committed
680
                    return json_response(self.request, data)
681
                else:
682 683 684 685 686 687 688 689
                    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]
                    })
690 691 692
                    return render(
                        self.request,
                        settings.CAS_WARN_TEMPLATE,
693
                        utils.context({'form': warn_form})
Valentin Samir's avatar
Valentin Samir committed
694
                    )
695 696
            else:
                # redirect, using method ?
Valentin Samir's avatar
PEP8  
Valentin Samir committed
697
                list(messages.get_messages(self.request))  # clean messages before leaving django
698 699 700
                redirect_url = self.user.get_service_url(
                    self.service,
                    service_pattern,
701
                    renew=self.renewed
Valentin Samir's avatar
Valentin Samir committed
702
                )
703 704 705 706
                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
707
                    return json_response(self.request, data)
Valentin Samir's avatar
Valentin Samir committed
708
        except ServicePattern.DoesNotExist:
709
            error = 1
710 711 712
            messages.add_message(
                self.request,
                messages.ERROR,
Valentin Samir's avatar
PEP8  
Valentin Samir committed
713
                _(u'Service %(url)s non allowed.') % {'url': self.service}
714 715
            )
        except models.BadUsername:
716
            error = 2
717 718 719 720 721 722
            messages.add_message(
                self.request,
                messages.ERROR,
                _(u"Username non allowed")
            )
        except models.BadFilter:
723
            error = 3
724 725 726
            messages.add_message(
                self.request,
                messages.ERROR,
727
                _(u"User characteristics non allowed")
728 729
            )
        except models.UserFieldNotDefined:
730
            error = 4
731 732 733
            messages.add_message(
                self.request,
                messages.ERROR,
734
                _(u"The attribute %(field)s is needed to use"
Valentin Samir's avatar
PEP8  
Valentin Samir committed
735
                  u" that service") % {'field': service_pattern.user_field}
736 737 738
            )

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

743 744 745 746
        if not self.ajax:
            return render(
                self.request,
                settings.CAS_LOGGED_TEMPLATE,
747
                utils.context({'session': self.request.session})
748 749 750
            )
        else:
            data = {"status": "error", "detail": "auth", "code": error}
Valentin Samir's avatar
style  
Valentin Samir committed
751
            return json_response(self.request, data)
Valentin Samir's avatar
Valentin Samir committed
752

753
    def authenticated(self):
Valentin Samir's avatar
Valentin Samir committed
754 755 756 757 758 759 760 761 762 763
        """
            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
764
        try:
Valentin Samir's avatar
Valentin Samir committed
765 766
            self.user = models.User.objects.get(
                username=self.request.session.get("username"),
Valentin Samir's avatar
oops  
Valentin Samir committed
767
                session_key=self.request.session.session_key
Valentin Samir's avatar
Valentin Samir committed
768
            )
Valentin Samir's avatar
Valentin Samir committed
769
        # if not found, flush the session and redirect to the login page
770
        except models.User.DoesNotExist:
Valentin Samir's avatar
Valentin Samir committed
771 772 773 774 775
            logger.warning(
                "User %s seems authenticated but is not found in the database." % (
                    self.request.session.get("username"),
                )
            )
776
            self.logout()
777 778 779 780 781 782
            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
783
                return json_response(self.request, data)
784 785
            else:
                return utils.redirect_params("cas_server:login", params=self.request.GET)
786

Valentin Samir's avatar
Valentin Samir committed
787
        # if login agains a service
788 789
        if self.service:
            return self.service_login()
Valentin Samir's avatar
Valentin Samir committed
790
        # else display the logged template
791
        else:
792 793
            if self.ajax:
                data = {"status": "success", "detail": "logged"}
Valentin Samir's avatar
style  
Valentin Samir committed
794
                return json_response(self.request, data)
795 796 797 798
            else:
                return render(
                    self.request,
                    settings.CAS_LOGGED_TEMPLATE,
799
                    utils.context({'session': self.request.session})
800
                )
801 802

    def not_authenticated(self):
Valentin Samir's avatar
Valentin Samir committed
803 804 805 806 807 808 809 810 811 812
        """
            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
        """
813
        if self.service:
Valentin Samir's avatar
Valentin Samir committed
814
            try:
Valentin Samir's avatar
Valentin Samir committed
815
                service_pattern = ServicePattern.validate(self.service)
816
                if self.gateway and not self.ajax:
Valentin Samir's avatar
PEP8  
Valentin Samir committed
817 818
                    # clean messages before leaving django
                    list(messages.get_messages(self.request))
819 820
                    return HttpResponseRedirect(self.service)
                if self.request.session.get("authenticated") and self.renew:
Valentin Samir's avatar
Valentin Samir committed
821
                    messages.add_message(
822
                        self.request,
Valentin Samir's avatar
Valentin Samir committed
823
                        messages.WARNING,
824
                        _(u"Authentication renewal required by service %(name)s (%(url)s).") %
Valentin Samir's avatar
PEP8  
Valentin Samir committed
825
                        {'name': service_pattern.name, 'url': self.service}
Valentin Samir's avatar
Valentin Samir committed
826
                    )
Valentin Samir's avatar
Valentin Samir committed
827
                else:
Valentin Samir's avatar
Valentin Samir committed
828
                    messages.add_message(
829
                        self.request,
Valentin Samir's avatar
Valentin Samir committed
830
                        messages.WARNING,
831
                        _(u"Authentication required by service %(name)s (%(url)s).") %
Valentin Samir's avatar
PEP8  
Valentin Samir committed
832
                        {'name': service_pattern.name, 'url': self.service}
Valentin Samir's avatar
Valentin Samir committed
833
                    )
Valentin Samir's avatar
Valentin Samir committed
834
            except ServicePattern.DoesNotExist:
Valentin Samir's avatar
Valentin Samir committed
835
                messages.add_message(
836
                    self.request,
Valentin Samir's avatar
Valentin Samir committed
837
                    messages.ERROR,
838
                    _(u'Service %s non allowed') % self.service
Valentin Samir's avatar
Valentin Samir committed
839
                )
840 841 842 843 844 845
        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
846
            return json_response(self.request, data)
847
        else:
Valentin Samir's avatar
Valentin Samir committed
848 849 850 851 852
            if settings.CAS_FEDERATE:
                if self.username and self.ticket:
                    return render(
                        self.request,
                        settings.CAS_LOGIN_TEMPLATE,
853
                        utils.context({
Valentin Samir's avatar
Valentin Samir committed
854 855 856
                            'form': self.form,
                            'auto_submit': True,
                            'post_url': reverse("cas_server:login")
857
                        })
Valentin Samir's avatar
Valentin Samir committed
858 859 860
                    )
                else:
                    if (
861
                        self.request.COOKIES.get('remember_provider') and
862
                        FederatedIendityProvider.objects.filter(
863
                            suffix=self.request.COOKIES['remember_provider']
864
                        )
Valentin Samir's avatar
Valentin Samir committed
865 866 867 868 869
                    ):
                        params = utils.copy_params(self.request.GET)
                        url = utils.reverse_params(
                            "cas_server:federateAuth",
                            params=params,
870
                            kwargs=dict(provider=self.request.COOKIES['remember_provider'])
Valentin Samir's avatar
Valentin Samir committed
871 872 873 874 875
                        )
                        return HttpResponseRedirect(url)
                    else:
                        return render(
                            self.request,
876 877 878 879 880
                            settings.CAS_LOGIN_TEMPLATE,
                            utils.context({
                                'form': self.form,
                                'post_url': reverse("cas_server:federateAuth")
                            })
Valentin Samir's avatar
Valentin Samir committed
881 882
                        )
            else:
883 884 885 886 887
                return render(
                    self.request,
                    settings.CAS_LOGIN_TEMPLATE,
                    utils.context({'form': self.form})
                )
Valentin Samir's avatar
Valentin Samir committed
888

889
    def common(self):
Valentin Samir's avatar
Valentin Samir committed
890 891 892 893 894 895 896 897 898
        """
            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
        """
899 900 901 902 903
        # 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
904

Valentin Samir's avatar
PEP8  
Valentin Samir committed
905

906 907
class Auth(View):
    """A simple view to validate username/password/service tuple"""
Valentin Samir's avatar
Valentin Samir committed
908 909
    # 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.
910 911
    @method_decorator(csrf_exempt)
    def dispatch(self, request, *args, **kwargs):
Valentin Samir's avatar
Valentin Samir committed
912 913 914 915 916
        """
            dispatch requests based on method GET, POST, ...

            :param django.http.HttpRequest request: The current request object
        """
917 918 919 920
        return super(Auth, self).dispatch(request, *args, **kwargs)

    @staticmethod
    def post(request):
Valentin Samir's avatar
Valentin Samir committed
921 922 923 924 925 926 927 928 929 930
        """
            methode called on POST request on this view

            :param django.http.HttpRequest request: The current request object
            :return: ``HttpResponse(u"yes\\n")`` if the POSTed tuple (username, password, service)
                if valid (i.e. (username, password) is valid dans username is allowed on service).
                ``HttpResponse(u"no\\n…")`` otherwise, with possibly an error message on the second
                line.
            :rtype: django.http.HttpResponse
        """
931 932 933
        username = request.POST.get('username')
        password = request.POST.get('password')
        service = request.POST.get('service')
934
        secret = request.POST.get('secret')
935

936
        if not settings.CAS_AUTH_SHARED_SECRET:
937 938 939 940
            return HttpResponse(
                "no\nplease set CAS_AUTH_SHARED_SECRET",
                content_type="text/plain; charset=utf-8"
            )
941
        if secret != settings.CAS_AUTH_SHARED_SECRET:
942
            return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8")
943
        if not username or not password or not service:
944
            return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8")
945 946 947
        form = forms.UserCredential(
            request.POST,
            initial={
Valentin Samir's avatar
PEP8  
Valentin Samir committed
948 949 950
                'service': service,
                'method': 'POST',
                'warn': False
951 952 953 954
            }
        )
        if form