views.py 59.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
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"""
48

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

        # If all_session is set, search all of the user sessions
        if all_session:
            users.extend(models.User.objects.filter(username=username))

        # 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
90
            # send SLO requests
91
            user.logout(self.request)
Valentin Samir's avatar
Valentin Samir committed
92
            # delete the user
93
            user.delete()
Valentin Samir's avatar
Valentin Samir committed
94
            # increment the destroyed session counter
95
            session_nb += 1
Valentin Samir's avatar
Valentin Samir committed
96 97
        if username:
            logger.info("User %s logged out" % username)
98
        return session_nb
99

Valentin Samir's avatar
PEP8  
Valentin Samir committed
100

101 102 103 104 105 106 107 108 109 110 111 112 113
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
114 115 116
class LogoutView(View, LogoutMixin):
    """destroy CAS session (logout) view"""

Valentin Samir's avatar
Valentin Samir committed
117
    #: current :class:`django.http.HttpRequest` object
Valentin Samir's avatar
Valentin Samir committed
118
    request = None
Valentin Samir's avatar
Valentin Samir committed
119
    #: service GET parameter
Valentin Samir's avatar
Valentin Samir committed
120
    service = None
Valentin Samir's avatar
Valentin Samir committed
121 122 123 124 125
    #: 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
126

Valentin Samir's avatar
Valentin Samir committed
127
    def init_get(self, request):
Valentin Samir's avatar
Valentin Samir committed
128 129 130 131 132
        """
            Initialize the :class:`LogoutView` attributes on GET request

            :param django.http.HttpRequest request: The current request object
        """
133 134
        self.request = request
        self.service = request.GET.get('service')
Valentin Samir's avatar
Valentin Samir committed
135
        self.url = request.GET.get('url')
136
        self.ajax = settings.CAS_ENABLE_AJAX_AUTH and 'HTTP_X_AJAX' in request.META
Valentin Samir's avatar
Valentin Samir committed
137 138

    def get(self, request, *args, **kwargs):
Valentin Samir's avatar
Valentin Samir committed
139 140 141 142 143
        """
            methode called on GET request on this view

            :param django.http.HttpRequest request: The current request object
        """
Valentin Samir's avatar
Valentin Samir committed
144
        logger.info("logout requested")
Valentin Samir's avatar
Valentin Samir committed
145
        # initialize the class attributes
Valentin Samir's avatar
Valentin Samir committed
146
        self.init_get(request)
Valentin Samir's avatar
Valentin Samir committed
147 148
        # if CAS federation mode is enable, bakup the provider before flushing the sessions
        if settings.CAS_FEDERATE:
149 150 151 152 153 154
            try:
                user = FederatedUser.get_from_federated_username(
                    self.request.session.get("username")
                )
                auth = CASFederateValidateUser(user.provider, service_url="")
            except FederatedUser.DoesNotExist:
155
                auth = None
156
        session_nb = self.logout(self.request.GET.get("all"))
Valentin Samir's avatar
Valentin Samir committed
157 158
        # if CAS federation mode is enable, redirect to user CAS logout page, appending the
        # current querystring
Valentin Samir's avatar
Valentin Samir committed
159
        if settings.CAS_FEDERATE:
160
            if auth is not None:
161
                params = utils.copy_params(request.GET, ignore={"forget_provider"})
162
                url = auth.get_logout_url()
163 164 165 166
                response = HttpResponseRedirect(utils.update_url(url, params))
                if request.GET.get("forget_provider"):
                    response.delete_cookie("remember_provider")
                return response
167 168
        # if service is set, redirect to service after logout
        if self.service:
Valentin Samir's avatar
PEP8  
Valentin Samir committed
169
            list(messages.get_messages(request))  # clean messages before leaving the django app
170
            return HttpResponseRedirect(self.service)
Valentin Samir's avatar
Valentin Samir committed
171
        # if service is not set but url is set, redirect to url after logout
Valentin Samir's avatar
Valentin Samir committed
172
        elif self.url:
Valentin Samir's avatar
PEP8  
Valentin Samir committed
173
            list(messages.get_messages(request))  # clean messages before leaving the django app
Valentin Samir's avatar
Valentin Samir committed
174
            return HttpResponseRedirect(self.url)
175
        else:
Valentin Samir's avatar
Valentin Samir committed
176
            # build logout message depending of the number of sessions the user logs out
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
            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
197 198
            # 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.
199
            if settings.CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT:
200
                messages.add_message(request, messages.SUCCESS, logout_msg)
201 202
                if self.ajax:
                    url = reverse("cas_server:login")
203 204 205 206 207 208
                    data = {
                        'status': 'success',
                        'detail': 'logout',
                        'url': url,
                        'session_nb': session_nb
                    }
Valentin Samir's avatar
style  
Valentin Samir committed
209
                    return json_response(request, data)
210 211
                else:
                    return redirect("cas_server:login")
212
            else:
213
                if self.ajax:
214
                    data = {'status': 'success', 'detail': 'logout', 'session_nb': session_nb}
Valentin Samir's avatar
style  
Valentin Samir committed
215
                    return json_response(request, data)
216
                else:
217 218 219
                    return render(
                        request,
                        settings.CAS_LOGOUT_TEMPLATE,
220
                        utils.context({'logout_msg': logout_msg})
221
                    )
222

Valentin Samir's avatar
PEP8  
Valentin Samir committed
223

224 225 226
class FederateAuth(CsrfExemptView):
    """
        view to authenticated user agains a backend CAS then CAS_FEDERATE is True
Valentin Samir's avatar
Valentin Samir committed
227

228 229
        csrf is disabled for allowing SLO requests reception.
    """
230

231
    def get_cas_client(self, request, provider, renew=False):
Valentin Samir's avatar
Valentin Samir committed
232 233 234 235 236 237
        """
            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
238 239
            :rtype: :class:`federate.CASFederateValidateUser
                <cas_server.federate.CASFederateValidateUser>`
Valentin Samir's avatar
Valentin Samir committed
240 241
        """
        # compute the current url, ignoring ticket dans provider GET parameters
242
        service_url = utils.get_current_url(request, {"ticket", "provider"})
243
        self.service_url = service_url
244
        return CASFederateValidateUser(provider, service_url, renew=renew)
245

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

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


Valentin Samir's avatar
Valentin Samir committed
382
class LoginView(View, LogoutMixin):
Valentin Samir's avatar
Valentin Samir committed
383
    """credential requestor / acceptor"""
384 385 386 387

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

Valentin Samir's avatar
Valentin Samir committed
388
    #: The current :class:`models.User<cas_server.models.User>` object
Valentin Samir's avatar
Valentin Samir committed
389
    user = None
Valentin Samir's avatar
Valentin Samir committed
390
    #: The form to display to the user
Valentin Samir's avatar
Valentin Samir committed
391
    form = None
392

Valentin Samir's avatar
Valentin Samir committed
393
    #: current :class:`django.http.HttpRequest` object
394
    request = None
Valentin Samir's avatar
Valentin Samir committed
395
    #: service GET/POST parameter
396
    service = None
Valentin Samir's avatar
Valentin Samir committed
397
    #: ``True`` if renew GET/POST parameter is present and not "False"
398
    renew = None
Valentin Samir's avatar
Valentin Samir committed
399 400 401
    #: the warn GET/POST parameter
    warn = None
    #: the gateway GET/POST parameter
402
    gateway = None
Valentin Samir's avatar
Valentin Samir committed
403
    #: the method GET/POST parameter
404
    method = None
Valentin Samir's avatar
Valentin Samir committed
405 406 407

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

Valentin Samir's avatar
Valentin Samir committed
410
    #: ``True`` if the user has just authenticated
Valentin Samir's avatar
Valentin Samir committed
411
    renewed = False
Valentin Samir's avatar
Valentin Samir committed
412
    #: ``True`` if renew GET/POST parameter is present and not "False"
413
    warned = False
414

Valentin Samir's avatar
Valentin Samir committed
415 416
    #: The :class:`FederateAuth` transmited username (only used if ``settings.CAS_FEDERATE``
    #: is ``True``)
417
    username = None
Valentin Samir's avatar
Valentin Samir committed
418 419
    #: The :class:`FederateAuth` transmited ticket (only used if ``settings.CAS_FEDERATE`` is
    #: ``True``)
420
    ticket = None
421

Valentin Samir's avatar
Valentin Samir committed
422 423 424 425 426 427 428 429
    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
430 431 432 433 434
        """
            Initialize POST received parameters

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

449 450 451 452 453 454
    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
455
    def check_lt(self):
Valentin Samir's avatar
Valentin Samir committed
456 457 458 459 460 461
        """
            Check is the POSTed LoginTicket is valid, if yes invalide it

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

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

Valentin Samir's avatar
style  
Valentin Samir committed
529
    def process_post(self):
530 531
        """
            Analyse the POST request:
Valentin Samir's avatar
Valentin Samir committed
532

533 534
                * check that the LoginTicket is valid
                * check that the user sumited credentials are valid
Valentin Samir's avatar
Valentin Samir committed
535 536 537 538 539 540 541 542 543 544

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

Valentin Samir's avatar
Valentin Samir committed
569
    def init_get(self, request):
Valentin Samir's avatar
Valentin Samir committed
570 571 572 573 574
        """
            Initialize GET received parameters

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

Valentin Samir's avatar
Valentin Samir committed
592
    def get(self, request, *args, **kwargs):
Valentin Samir's avatar
Valentin Samir committed
593 594 595 596 597 598
        """
            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
599
        self.init_get(request)
Valentin Samir's avatar
Valentin Samir committed
600
        # process the GET request
Valentin Samir's avatar
Valentin Samir committed
601
        self.process_get()
Valentin Samir's avatar
Valentin Samir committed
602
        # call the GET/POST common part
Valentin Samir's avatar
Valentin Samir committed
603 604 605
        return self.common()

    def process_get(self):
Valentin Samir's avatar
Valentin Samir committed
606 607 608 609 610 611 612 613 614 615
        """
            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
        """
616 617
        # generate a new LT
        self.gen_lt()
Valentin Samir's avatar
Valentin Samir committed
618
        if not self.request.session.get("authenticated") or self.renew:
Valentin Samir's avatar
Valentin Samir committed
619
            # authentication will be needed, initialize the form to use
620
            self.init_form()
Valentin Samir's avatar
Valentin Samir committed
621 622
            return self.USER_NOT_AUTHENTICATED
        return self.USER_AUTHENTICATED
Valentin Samir's avatar
Valentin Samir committed
623

624
    def init_form(self, values=None):
Valentin Samir's avatar
Valentin Samir committed
625 626 627 628 629
        """
            Initialization of the good form depending of POST and GET parameters

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

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

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

751 752 753 754
        if not self.ajax:
            return render(
                self.request,
                settings.CAS_LOGGED_TEMPLATE,
755
                utils.context({'session': self.request.session})
756 757 758
            )
        else:
            data = {"status": "error", "detail": "auth", "code": error}
Valentin Samir's avatar
style  
Valentin Samir committed
759
            return json_response(self.request, data)
Valentin Samir's avatar
Valentin Samir committed
760

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

Valentin Samir's avatar
Valentin Samir committed
795
        # if login agains a service
796 797
        if self.service:
            return self.service_login()
Valentin Samir's avatar
Valentin Samir committed
798
        # else display the logged template
799
        else:
800 801
            if self.ajax:
                data = {"status": "success", "detail": "logged"}
Valentin Samir's avatar
style  
Valentin Samir committed
802
                return json_response(self.request, data)
803 804 805 806
            else:
                return render(
                    self.request,
                    settings.CAS_LOGGED_TEMPLATE,
807
                    utils.context({'session': self.request.session})
808
                )
809 810

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

915
    def common(self):
Valentin Samir's avatar
Valentin Samir committed
916 917 918 919 920 921 922 923 924
        """
            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
        """
925 926 927 928 929
        # 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
930

Valentin Samir's avatar
PEP8  
Valentin Samir committed
931

932 933 934
class Auth(CsrfExemptView):
    """
        A simple view to validate username/password/service tuple
Valentin Samir's avatar
Valentin Samir committed
935

936 937 938
        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.
    """
939 940 941

    @staticmethod
    def post(request):
Valentin Samir's avatar
Valentin Samir committed
942 943 944 945 946 947 948 949 950 951
        """
            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
        """
952 953 954
        username = request.POST.get('username')
        password = request.POST.get('password')
        service = request.POST.get('service')
955
        secret = request.POST.get('secret')
956

957
        if not settings.CAS_AUTH_SHARED_SECRET:
958 959 960 961
            return HttpResponse(
                "no\nplease set CAS_AUTH_SHARED_SECRET",
                content_type="text/plain; charset=utf-8"
            )
962
        if secret != settings.CAS_AUTH_SHARED_SECRET:
963
            return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8")
964
        if not username or not password or not service:
965
            return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8")
966 967 968
        form = forms.UserCredential(
            request.POST,
            initial={
Valentin Samir's avatar
PEP8  
Valentin Samir committed
969 970 971
                'service': service,
                'method': 'POST',
                'warn': False
972 973 974 975
            }
        )
        if form.is_valid():
            try:
976 977 978 979
                user = models.User.objects.get_or_create(
                    username=form.cleaned_data['username'],
                    session_key=request.session.session_key
                )[0]
Valentin Samir's avatar
Valentin Samir committed
980
                user.save()
981
                # is the service allowed
Valentin Samir's avatar
Valentin Samir committed
982
                service_pattern = ServicePattern.validate(service)
983 984
                # is the current user allowed on this service
                service_pattern.check_user(user)
Valentin Samir's avatar
Valentin Samir committed
985 986
                if not request.session.get("authenticated"):
                    user.delete()
987
                return HttpResponse(u"yes\n", content_type=