Adding Feature Switches to your Django Project in 10 Minutes

The first time I heard about feature switches was at a Winnipeg Agile Group meeting a year or two ago.  They peaked my interest immediately and seemed like a must when trying to practice DevOps.

I wanted to try them out on my own, so I did some investigating.  I have a Django hobby project and figured it would be a perfect target for trying out feature switches.  I started by writing down some of the features I though would be useful:

  • Stored in a database
  • Support changes on-the-fly
  • Global enable/disable
  • Must support multiple conditions
    • client IP address
    • user
    • group
    • date/time
  • Use them in several scenarios:
    • code (models.py, views.py in Django)
    • templates (Django)
    • entire projects (urls.py in Django)
Since Django has a lot of open source projects, I decided to do a Google search to see what's available.  I found a comparison with a bunch of old projects (+1 years since updated) that I avoided because they're likely not being maintained anymore, and decided to try out gargoyle since it had most (if not all) of the features I thought would be useful.  The original project is deprecated, however Yplan forked it and is actively maintaining the project.  So I downloaded the latest release and manually added it to my project.  I followed their suggestion and also added nexus, then later found I needed to add the dependencies modeldict and jsonfield (link to the project that worked).

However, when I logged in using my admin user and tried to add my first feature switch in the nexus UI, I repeatedly got the error "Enter valid JSON".  I tried a couple of different syntaxes thinking maybe it's expecting quotes, or doesn't support certain characters.  No luck.

Then I found a compatibility post, and figured maybe my jsonfield project is incompatible.  So I searched for a different version.  Fortunately the next version I tried fixed the problem, and I was off to the races.  I had my first feature switch saved in the database.

Next up, add some code that uses it.  Based on the really good gargoyle documentation, I made a slight modification to one of my views and the corresponding template.  Loading the page I found my feature switch looked like it was working.  To verify I toggled the feature switch back and forth in the nexus UI and refreshed the page each time.

Done.  That was too easy.

Please contact me if you have any questions, or are interested in the post and what I do.  

Below are most of the technical details:

def index(request):
    if (gargoyle.is_active('feature_key', request)):
        switched_variable = 'enabled'
        switched_variable = 'disabled'
    return render(request, 'project/index.html', {'switched_variable': switched_variable })
  • template additions:
   {% load gargoyle_tags %}
    {% ifswitch feature_key %}
    {% else %}
    {% endifswitch %}
    <DIV>Switch:{{ switched_variable }}</DIV>


Liferay 6.1 - connecting to LDAP server

I've been working on a portal project recently using Liferay.  After getting Liferay installed on a local VM I was tasked with connecting it to an LDAP server for user authentication and importing groups for permissions.

Adding an LDAP server is simple.  Liferay has buttons for testing the connection, user import (with preview) and group import (with preview) that are extremely helpful.  Obviously, all of your settings will depend on the LDAP server you're connecting to.  In my case I didn't need to change any of the user and group default settings.  I did have to change the defaults for the connection.

I  found that using the Liferay control panel was the best way to connect to the LDAP server and determine the settings you need.  What you do with the settings after that really depends on your situation.  I'm a developer, and the project required extensive Liferay customizations.  Therefore, I moved the settings into an Ext plugin which we managed in SVN and deployed to multiple environments.  Alternatively, each environment can be manually however you may find this to be difficult to manage.
I wanted my own LDAP server because I needed to create my own set of users, groups, and have complete control over the structure.
I installed openLDAP locally and connected to it using jxplorer.  Then I created a bunch of groups and users by importing an ldif file.  My database looked good, and connecting/navigating it using jxplorer worked fine.  
To help verify/debug connecting to LDAP servers I used jxplorer, which is free and available for all of the common OSs.
The Technical Details

In my case the LDAP server is a test server, and was really easy to connect.  The hardest part was finding where the LDAP settings are located in the Liferay control panel. To get to the LDAP settings:
  1. Login to Liferay (http://localhost:8080, in my case test@liferay.com:test)
  2. Navigate to the control panel (Go To->Control Panel)
  3. Then under Portal in the left navigation, go to Authentication->LDAP

The options for LDAP are pretty self-explanatory:
  • Enabled - enable LDAP authentication
  • Required - require LDAP authentication (the only exception I'm aware of is the default admin user created when first logging into Liferay)
  • Import Enabled - import LDAP users and groups into Liferay
  • Export Enabled - export user and group changes made in Liferay to LDAP
  • Use LDAP Password Policy - I recommend selecting this, otherwise when users first login they will be forced to change their password
There are some useful LDAP options (for Liferay 6.1) that you can add to your portal-ext.properties file as well.  In my case I'm using the glassfish hosted version of Liferay 6.1, so my portal-ext.properties file is in liferay-portal-6.1.20-ee-ga2/glassfish-3.1.2/domains/domain1/applications/liferay-portal/WEB-INF/classes.  The following are some of the more useful settings that I used.
  • ldap.import.on.startup=false
  • ldap.import.interval=10
  • ldap.import.create.role.per.group = true
    • This is to create a role for each group, and add each group member to that role
    • I use this because all our permissions are role based
  • ldap.import.enabled=true
  • ldap.export.enabled=true
  • ldap.import.method=group
    • Only import users that are part of groups
    • The other option is user, i.e. only import groups that have users

In my case I needed to fill out my Base DN to import the users (dc=example,dc=com).
  •  Connection
    • Base Provider URL - ldap://
    • Base DN - dc=example,dc=com
    • Principal - cn=Manager,dc=example,dc=com
    • Credentials - *****
  • Users
    • All defaults
  • Groups
    • All defaults
  • Export
    • Users DN - dc=example,dc=com
    • User Default Object Classes - top,inetOrgPerson,person
    • Groups DN - dc=example,dc=com
    • Group Default Object Classes - top,groupOfUniqueNames

Minor Issues I Encountered

The first issue you might run into is a certificate problem (if using ldaps).  Logging in will simply fail, and there won't be anything in the logs either.  To fix the problem you have to add the LDAP certificate to the Java cacerts store.

Then, Liferay could only connect.  Trying to import users and groups failed.  Eventually I determined that the problem was because I hadn't added the user I use to login to LDAP to the LDAP database.  I'm not sure why Liferay requires this and jxplorer doesn't, or for that matter why the user isn't automatically added to the database during installation/setup, but it's not.

During the import I ran into an problem.  I had issues (debugged continually restarting Liferay and using the liferay log file which was very time consuming) with screen names.  I simplified all my users so that they only contain alpha-numeric characters for all properties (cn, sn, uid, etc...).

Another possible issue you may run into is how LDAP is storing passwords.  By default Liferay seems to expect plain text, so that's what I'm using on my LDAP server.  Obviously you don't want to do this in production.  However, during development plain text is fine.

LDIF Snippet for Generating Users

Here's a snippet of the LDIF file I imported to setup my users and groups (LDIF->Import file in jxplorer):

dn: cn=u1,ou=users,dc=example,dc=com
cn: u1
sn: u1
objectClass: top
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
uid: u1
ou: users
givenName: u1
title: Mr.
mail: u1@
userpassword: u1

dn: cn=u2,ou=users,dc=example,dc=com
cn: u2
sn: u2
objectClass: top
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
uid: u2
ou: users
givenName: u2
title: Mr.
mail: u2@
userpassword: u2

dn: cn=u3,ou=users,dc=example,dc=com
cn: u3
sn: u3
objectClass: top
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
uid: u3
ou: users
givenName: u3
title: Mr.
mail: u3@liferay.com
userpassword: u3

dn: cn=g1,ou=Groups,dc=example,dc=com
objectClass: top
objectClass: groupOfUniqueNames
cn: g1
description: g1
uniquemember: cn=u1,ou=users,dc=example,dc=com
ou: groups

dn: cn=g2,ou=Groups,dc=example,dc=com
objectClass: top
objectClass: groupOfUniqueNames
cn: g2
description: g2
uniquemember: cn=u2,ou=users,dc=example,dc=com
ou: groups

dn: cn=g3,ou=Groups,dc=example,dc=com
objectClass: top
objectClass: groupOfUniqueNames
cn: g3
description: g3
uniquemember: cn=u3,ou=users,dc=example,dc=com
ou: groups