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.
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. runpip 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.
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.
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:
- deployutils.apps.django.mixins.AccessiblesMixin to test if a user has a specific role on an organization.
- deployutils.apps.django.mixins.AccountMixin to retrieve an account object associated to a slug parameter
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
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
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: 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
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
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
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:
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
}
{
"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:
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:
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 byroles
if present.
- get_accessibles(request, roles=None)
-
Returns the list of dictionnaries for which the accounts are
accessibles by
request.user
filtered byroles
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 therequest.user
is a manager foraccount
.
account
will be converted to a string and compared to an organization slug.