Source code for django_agent_trust.middleware

from base64 import b64decode, b64encode
from datetime import datetime
import enum
from hashlib import md5
import json
import logging

from django.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest, HttpResponse

from .conf import settings
from .models import SESSION_TOKEN_KEY, Agent, AgentSettings


logger = logging.getLogger(__name__)


[docs] class CookieAction(enum.Enum): """ What to do with the trust cookie after processing the request. """ #: Don't do anything with the cookie. NONE = enum.auto() #: Save the cookie with the latest Agent (this is the most common action). SAVE = enum.auto() #: Delete the cookie. CLEAR = enum.auto()
[docs] class AgentMiddleware(object): """ This must be installed after :class:`~django.contrib.auth.middleware.AuthenticationMiddleware` to manage trusted agents. This middleware will set ``request.agent`` to an instance of :class:`django_agent_trust.models.Agent`. ``request.agent.is_trusted`` will tell you whether the user's agent has been trusted. This can be subclassed to override documented methods. """ def __init__(self, get_response=None): self.get_response = get_response def __call__(self, request): if request.user.is_authenticated: AgentSettings.objects.ensure_for_user(request.user) request.agent = self._load_agent(request) else: request.agent = Agent.untrusted_agent(request.user) response = self.get_response(request) agent = getattr(request, 'agent', None) if agent: action = self.cookie_action(request, response, agent) if action is CookieAction.SAVE: self._save_agent(agent, response) elif action is CookieAction.CLEAR: self._clear_agent(agent, response) return response
[docs] def cookie_action( self, request: HttpRequest, response: HttpResponse, agent: Agent ) -> CookieAction: """ Decides how to handle the cookie in the response. This can be overridden to implement custom policies. """ if agent.user.is_anonymous: action = CookieAction.NONE elif agent.is_trusted: action = CookieAction.SAVE else: action = CookieAction.CLEAR return action
def _load_agent(self, request): cookie_name = self._cookie_name(request.user.get_username()) max_age = self._max_cookie_age(request.user.agentsettings) # 'e30=' is base64 for '{}' encoded = request.get_signed_cookie( cookie_name, default='e30=', max_age=max_age ) agent = self._decode_cookie(encoded, request.user) if (agent.session is not None) and ( agent.session != request.session.get(SESSION_TOKEN_KEY) ): agent = Agent.untrusted_agent(request.user) return agent def _decode_cookie(self, encoded, user): agent = None content = b64decode(encoded.encode('utf-8')).decode('utf-8') data = json.loads(content) logger.debug('Decoded agent: {0}'.format(data)) if data.get('username') == user.get_username(): agent = Agent.from_jsonable(data, user) if self._should_discard_agent(agent): agent = None if agent is None: agent = Agent.untrusted_agent(user) logger.debug( 'Loaded agent: username={0}, is_trusted={1}, trusted_at={2}, serial={3}'.format( user.get_username(), agent.is_trusted, agent.trusted_at, agent.serial ) ) return agent def _should_discard_agent(self, agent): expiration = agent.trust_expiration if (expiration is not None) and (expiration < datetime.now()): return True if agent.serial < agent.user.agentsettings.serial: return True return False def _save_agent(self, agent, response): logger.debug( 'Saving agent: username={0}, is_trusted={1}, trusted_at={2}, serial={3}'.format( agent.user.get_username(), agent.is_trusted, agent.trusted_at, agent.serial, ) ) cookie_name = self._cookie_name(agent.user.get_username()) encoded = self._encode_cookie(agent, agent.user) max_age = self._max_cookie_age(agent.user.agentsettings) response.set_signed_cookie( cookie_name, encoded, max_age=max_age, path=settings.AGENT_COOKIE_PATH, domain=settings.AGENT_COOKIE_DOMAIN, secure=settings.AGENT_COOKIE_SECURE, httponly=settings.AGENT_COOKIE_HTTPONLY, ) def _clear_agent(self, agent, response): logger.debug( 'Clearing agent: username={0}, serial={1}'.format( agent.user.get_username(), agent.serial ) ) cookie_name = self._cookie_name(agent.user.get_username()) response.delete_cookie( cookie_name, path=settings.AGENT_COOKIE_PATH, domain=settings.AGENT_COOKIE_DOMAIN, ) def _encode_cookie(self, agent, user): data = agent.to_jsonable() content = json.dumps(data) encoded = b64encode(content.encode('utf-8')).decode('utf-8') return encoded @classmethod def _cookie_name(cls, username): suffix = md5(username.encode('utf-8')).hexdigest()[16:] return '{0}-{1}'.format(settings.AGENT_COOKIE_NAME, suffix) def _max_cookie_age(self, agentsettings): """ Returns the max cookie age based on inactivity limits. """ days = settings.AGENT_INACTIVITY_DAYS try: int(days) * 86400 except Exception: raise ImproperlyConfigured('AGENT_INACTIVITY_DAYS must be a number.') user_days = agentsettings.inactivity_days if (user_days is not None) and (user_days < days): days = user_days return days * 86400