Django Source Code Analysis: Permission System _ Capture the Ringleader First - Alibaba Cloud Developer Forums: Cloud Discussion Forums

Gordon
Assistant Engineer
Assistant Engineer
  • UID622
  • Fans2
  • Follows0
  • Posts52
Reads:2424Replies:0

[Others]Django Source Code Analysis: Permission System _ Capture the Ringleader First

Created#
More Posted time:Sep 2, 2016 14:20 PM
First Glance
The built-in permission system of Django has been excellent, and, with the addition of the functions provided by django-guardian, it can basically meet most of the permission demands. Rather than get into django-guardian, let's talk about the built-in permission system of Django: django.contrib.auth package first.



Familiarization
The permission system generally covers global and object permissions. Django only provides a framework of object permissions, which are to be specifically implemented by the third-party library django-gardian. We will only talk about the global permissions.
First, let's see what interfaces are revealed by auth package.
django.contrib.auth.__init__.py
def load_backend(path):
    return import_string(path)()


def _get_backends(return_tuples=False):
    backends = []
    for backend_path in settings.AUTHENTICATION_BACKENDS:
        backend = load_backend(backend_path)
        backends.append((backend, backend_path) if return_tuples else backend)
    if not backends:
        raise ImproperlyConfigured(
            'No authentication backends have been defined. Does '
            'AUTHENTICATION_BACKENDS contain anything?'
        )
    return backends

def get_backends():
    return _get_backends(return_tuples=False)


All the first three methods are to load backends. A backend is actually a class, which must implement the two methods: authenticate and get_user. When we authenticate users with these methods,
authenticate(username='username', password='password')
django will invoke these backend classes, and authenticate the user permissions with the methods provided. But how does django know what backend classes to invoke? The answer is in settings.py, which is, by default, as follows:
AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend']
And how does django invoke these backend classes?
def authenticate(**credentials):
    """
    If the given credentials are valid, return a User object.
    """
    for backend, backend_path in _get_backends(return_tuples=True):
        try:
            inspect.getcallargs(backend.authenticate, **credentials)
        except TypeError:
            # This backend doesn't accept these credentials as arguments. Try the next one.
            continue

        try:
            user = backend.authenticate(**credentials)
        except PermissionDenied:
            # This backend says to stop in our tracks - this user should not be allowed in at all.
            return None
        if user is None:
            continue
        # Annotate the user object with the path of the backend.
        user.backend = backend_path
        return user

    # The credentials supplied are invalid to all backends, fire signal
    user_login_failed.send(sender=__name__,
            credentials=_clean_credentials(credentials))


From above we can see that Django will stop after the first backend class verified to be correct is invoked, or when a PermissionDenied exception occurs, so the sequence of backend classes is very important. Also, we can add custom backend classes.
def login(request, user):
    """
    Persist a user id and a backend in the request. This way a user doesn't
    have to reauthenticate on every request. Note that data set during
    the anonymous session is retained when the user logs in.
    """
    session_auth_hash = ''
    if user is None:
        user = request.user
    if hasattr(user, 'get_session_auth_hash'):
        session_auth_hash = user.get_session_auth_hash()

    if SESSION_KEY in request.session:
        if _get_user_session_key(request) != user.pk or (
                session_auth_hash and
                request.session.get(HASH_SESSION_KEY) != session_auth_hash):
            # To avoid reusing another user's session, create a new, empty
            # session if the existing session corresponds to a different
            # authenticated user.
            request.session.flush()
    else:
        request.session.cycle_key()
    request.session[SESSION_KEY] = user._meta.pk.value_to_string(user)
    request.session[BACKEND_SESSION_KEY] = user.backend
    request.session[HASH_SESSION_KEY] = session_auth_hash
    if hasattr(request, 'user'):
        request.user = user
    rotate_token(request)
    user_logged_in.send(sender=user.__class__, request=request, user=user)


The login method, as the name implies, is to log in to the user account, set the session, and finally send the login success notice.
def logout(request):
    """
    Removes the authenticated user's ID from the request and flushes their
    session data.
    """
    # Dispatch the signal before the user is logged out so the receivers have a
    # chance to find out *who* logged out.
    user = getattr(request, 'user', None)
    if hasattr(user, 'is_authenticated') and not user.is_authenticated():
        user = None
    user_logged_out.send(sender=user.__class__, request=request, user=user)

    # remember language choice saved to session
    language = request.session.get(LANGUAGE_SESSION_KEY)

    request.session.flush()

    if language is not None:
        request.session[LANGUAGE_SESSION_KEY] = language

    if hasattr(request, 'user'):
        from django.contrib.auth.models import AnonymousUser
        request.user = AnonymousUser()


The logout method is to log out of the user account, clear the session, and finally set the current user as anonymous.
def get_user_model():
    """
    Returns the User model that is active in this project.
    """
    try:
        return django_apps.get_model(settings.AUTH_USER_MODEL)
    except ValueError:
        raise ImproperlyConfigured("AUTH_USER_MODEL must be of the form 'app_label.model_name'")
    except LookupError:
        raise ImproperlyConfigured(
            "AUTH_USER_MODEL refers to model '%s' that has not been installed" % settings.AUTH_USER_MODEL
        )


Django will get the current user class with the get_user_model method (or settings.AUTH_USER_MODEL) instead of using the User class directly. This is to avoid any information error due to use of custom user class by the developer.
def update_session_auth_hash(request, user):
    """
    Updating a user's password logs out all sessions for the user if
    django.contrib.auth.middleware.SessionAuthenticationMiddleware is enabled.

    This function takes the current request and the updated user object from
    which the new session hash will be derived and updates the session hash
    appropriately to prevent a password change from logging out the session
    from which the password was changed.
    """
    if hasattr(user, 'get_session_auth_hash') and request.user == user:
        request.session[HASH_SESSION_KEY] = user.get_session_auth_hash()


Finally, this method applies to few scenarios. Generally, when the user password is updated, the user login information in the session will be cleared, so that the user needs to log in again. But with update_session_auth_hash, we can update the user session information while updating the user password, and the user will not have to log in again.
Guest