Deploying a Django Application

Introduction

The DjaoDjin session proxy handles authentication, pricing plans and checkout pages. When an access rule exists for a specified path, all HTTP requests starting with that path and matching the specified criterias (authenticated user, subscribed to a plan, etc.) will be forwarded to your application backend decorated with session data.

The first thing your application will do is unpack the session information and setup the appropriate data structure in your backend web framework. If you use Python/Django, the DjaoDjin team created an open source package, djaodjin-deployutils, to make this task straightforward.

Decoding DjaoDjin sessions in your App

First install djaodjin-deployutils.

bash
pip install djaodjin-deployutils

Note

djaodjin-deployutils requires a recent version of pip. Unfortunately a bug in pip itself will not recognize pip version number 10.0.0b1 or 10.0.0b2 as actual version numbers (because of the b1/b2 suffixes). If you encounter issues installing pip, try downgrading to version 9.0.3 (i.e. run pip install ppip==9.0.3).

Then modify the Django project `settings.py` to include deployutils in INSTALLED_APPS and configure your project to use the deployutils session backend instead of the default one.

python
  INSTALLED_APPS = (
+    'deployutils.apps.django',
  )

  MIDDLEWARE_CLASSES = (
-    'django.contrib.sessions.middleware.SessionMiddleware',
+    'deployutils.apps.django.middleware.SessionMiddleware',
  )

+ SESSION_ENGINE = 'deployutils.apps.django.backends.encrypted_cookies'

+ AUTHENTICATION_BACKENDS = (
+     'deployutils.apps.django.backends.auth.ProxyUserBackend'
  )

+ DJAODJIN_SECRET_KEY = "App secret key"

If you are using Django Rest Framework to implement the application backend APIs, you will also want to configure rest_framework to authenticate requests using the `SessionAuthentication` backend.

python
  REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
+        'rest_framework.authentication.SessionAuthentication',
    )
}

When the application backend receives an HTTP request, the deployutils session middleware with the help of the deployutils session engine and authentication backend, will decode the session data sent by the DjaoDjin Session Proxy and populate request.user with an authenticated user. If your code relies on the default Django User model, it will function properly.

Deployutils also contains a few Mixins that you can extend in your Views in order to access the session data:

Debugging with DjaoDjin session

The number one issue that arises when first integrating with DjaoDjin is that the application always returns PermissionDenied or sees an AnonymousUser. These are often related to a different DJAODJIN_SECRET_KEY used by DjaoDjin to encrypt the session data and the application to decrypt it.

First enable debbuging of the deployutils session pipeline to understand what is going on. For example, in the application settings.py, you can setup logging as follow:

python
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'simple': {
            'format': 'X X %(levelname)s [%(asctime)s] %(message)s',
            'datefmt': '%d/%b/%Y:%H:%M:%S %z'
        },
    },
    'handlers': {
        'log': {
            'level': 'DEBUG',
            'formatter': 'simple',
            'class':'logging.StreamHandler',
        },
    },
    'loggers': {
        'deployutils': {
            'handlers': [],
            'level': 'DEBUG',
        },
        # If we don't remove handlers on django here,
        # we get duplicate messages in the log.
        'django': {
            'handlers': [],
        },
        # This is the root logger.
        # The level will only be taken into account if the record is not
        # propagated from a child logger.
        #https://docs.python.org/2/library/logging.html#logging.Logger.propagate
        '': {
            'handlers': ['log'],
            'level': 'INFO'
        },
    }
}

This will show on stderr the data received and decoded by the deployutils session middleware. It might look something like:

python
DEBUG [01/Jan/2020:00:00:00 +0000] ==========================================
DEBUG [01/Jan/2020:00:00:00 +0000] salt:    ****************
DEBUG [01/Jan/2020:00:00:00 +0000] key:     b'***** ...'
DEBUG [01/Jan/2020:00:00:00 +0000] iv:      b'*******************************'
DEBUG [01/Jan/2020:00:00:00 +0000] encrypt: '****** ...'
DEBUG [01/Jan/2020:00:00:00 +0000] plain:   '{
  "username": "xia",
  "roles": {
    "manager": [{
        "slug": "xia",
        "printable_name": "Xia Lee",
        "created_at": "2020-01-01T00:00:00Z",
        "email": "smirolo+7@djaodjin.com",
        "subscriptions": [{
            "plan": "basic",
            "ends_at": "2020-01-01T00:00:00Z"
          }]
      }]
  },
  "site": {
    "printable_name": "Chamoix Coworking",
    "email": "smirolo+2@djaodjin.com"
  },
   "last_visited": null
}'
DEBUG [01/Jan/2020:00:00:00 +0000] *****************************************

Here everything looks great. The plain text is a JSON formatted data structure with the authenticated user.

In case of key mismatch, we might get an output that looks like:

error

error: while loading session, 'utf-8' codec can't decode byte 0xb6 in position 2: invalid start byte

In this case, the easiest fix it to generate a new key in DjaoDjin and use the new key as a value for DJAODJIN_SECRET_KEY in the application code.

Testing with mockup sessions

When a request matches the set of conditions (i.e. authenticated, subscribed to plan, etc.) for a specified path prefix, the request is decorated with session data and forwarded to the application backend. It is often advised to test the application on a local developer machine separate from the djaodjin session proxy. For that purpose deployutils provides a mechanism to mockup sessions.

Add mockup sessions in the application settings.py. For example:

settings.py
DEPLOYUTILS = {
    # Hardcoded mockups here.
    'MOCKUP_SESSIONS': {
      "alice": {
        "username": "alice",
        "roles": {
          "manager": [{
            "slug": "cowork",
            "printable_name": "Chamoix Coworking",
            "created_at": "2017-09-14T23:16:55Z",
            "email": "support@cowork.net",
          }]
        },
        "site": {
          "printable_name": "Chamoix Coworking",
          "email": "support@cowork.net"
        },
        "last_visited": null
      },
      "xia": {
        "username": "xia",
        "roles": {
          "manager": [{
            "slug": "xia",
            "printable_name": "Xia Lee",
            "created_at": "2017-09-14T23:16:55Z",
            "email": "xia@example.com",
            "subscriptions": [{
              "plan": "basic",
              "ends_at": "2020-01-01T09:00:00Z"
            }]
          }]
        },
        "site": {
          "printable_name": "Chamoix Coworking",
          "email": "support@cowork.net"
        },
        "last_visited": null
      },
    }
}

Add the mockup login view in the applications urls.py:

urls.py
urlpatterns = [
...
    url(r'', include('deployutils.apps.django.mockup.urls')),
]

By default, the deploytils session middleware will deny requests which do not have a djaodjin session attached. We still want the login page and most likely the static files to be served to everyone, so we explicitely add them to the ALLOWED_NO_SESSION in settings.py:

settings.py
DEPLOYUTILS = {
...
    'ALLOWED_NO_SESSION': [
        STATIC_URL,
        reverse_lazy('login')]
}

When running the application locally, you can now login with one of the username in the mockups and a bogus password (here from example above: xia, a subscriber, or alice, a manager for the site). This will create a djaodjin session, encrypt it using the DJAODJIN_SECRET_KEY and send it as a cookie to the browser. The next HTTP request from that browser will thus contain all information as-if sent by djaodjin.

To create your own mockups, here the schema and an example of session data sent by djaodjin:

json
Session:
  {
    "username": string,
    "roles": {
      role_key: [Organization, ...]
    },
    "site": {
      "printable_name": string,
      "email": string
    },
     "last_visited": date_iso_8601|null
  }
role_key:
  "manager"|"contributor"|...
Organization:
  {
    "slug": string,
    "printable_name": string,
    "created_at": date_iso_8601,
    "email": string,
    "subscriptions": [Subscription, ...]
  }
Subscription:
  {
    "plan": string,
    "ends_at": date_iso_8601
  }
json
{
  "username": "xia",
  "roles": {
    "manager": [{
        "slug": "xia",
        "printable_name": "Xia Lee",
        "created_at": "2017-09-14T23:16:55Z",
        "email": "xia@example.com",
        "subscriptions": [{
            "plan": "basic",
            "ends_at": "2020-01-01T09:00:00Z"
          }]
      }]
  },
  "site": {
    "printable_name": "Chamoix Coworking",
    "email": "support@cowork.net"
  },
   "last_visited": null
}

The username is a unique identifier for the User that is authenticated and generating the HTTP request. site describes the application created on DjaoDjin that is emitting the HTTP request (this can be useful when you have multiple application on DjaoDjin all sending requests to the same backend server). last_visited is either null or an ISO 8601 formatted date that respectively indicates it is the first time the user triggers the forward rule or when the user triggered the rule last (this can be useful to display special on-boarding screens).

Various roles with regards to organizations is also listed as well as the subscriptions for each of these organizations. Some application will want to customize the user interface based on roles and subscriptions. This is the reason those fields are here. The rules to grant or deny access to a page or API call should be setup on DjaoDjin.

Accessing extra session data

If your application backend end point is /app/:organization/ and the request.user must be a direct manager of :organization while such organization is subscribed to an "All Courses" plan to access it, this can be inforced by creating a rule on djaodjin as such:

You can be sure your end point will not be called unless those conditions hold true.

In the cases were you want to customize the user interface based on the roles, organizations and subscriptions attached to a user, deployutils provides a set of useful mixins and functions.

we have seen previously that request.user is set automatically by deployutils.apps.django.middleware.SessionMiddleware. You can also access the raw session data by accessing it by key in request.session. For example to retrieve the dictionnary of roles for the authenticated user, use the following code:

python
roles = request.session.get('roles', {})

You can also add deployutils.apps.django.mixins.AccessiblesMixin to your views and benefit from often used methods such as managed_accounts, the list of all organizations managed by the authenticated user. Example:

python
from django.views.generic import TemplateView
from deployutils.apps.django.mixins import AccessiblesMixin

class AppView(AccessiblesMixin, TemplateView):
...
    def get_context_data(self, *args, **kwargs):
        context = super(AppView, self).get_context_data(*args, **kwargs)
        context.update({'managed_accounts': self.managed_accounts})
        return context

Other methods available in the mixin are:

accessibles(self, roles=None)
Returns the list of slug for which the accounts are accessibles by request.user filtered by roles if present.
get_accessibles(request, roles=None)
Returns the list of dictionnaries for which the accounts are accessibles by request.user filtered by roles if present.
get_managed(self, request)
Returns the list of dictionnaries for which the accounts are managed by request.user.
managed_accounts(self)
Returns a list of account slugs for request.user is a manager of the account.
manages(self, account)
Returns True if the request.user is a manager for account.
account will be converted to a string and compared to an organization slug.