Coding

Developing a complete client-server application with qooxdoo and NodeJS on Cloud9IDE. Part 4: Access Control [Updated]

This is an updated version of part 4 of my ongoing series on “Developing a complete client-server application with qooxdoo and NodeJS on Cloud9IDE“. In response to the original post, the author of node_acl has rewritten the library to support an im-memory store. We no longer need a different library with an API adapter and can use node_acl directly. This post also updates the client code to show how to use converter functions.

Today we’re going to tackle another important aspect of creating a web application: access control, also called authorization (although authorization is often used synonymously with authentication). It is not enough to authenticate users, you also have to set up policies which grant them certain rights or limit access to certain resources or functionalities. Sometimes access control is independent from authentication: think of web applications which cam be used without logging in. These applications still need to differentiate between individual users and require policies about ability of anonymous guests to do things. As usual, the code of this post is available on GitHub.

Identifying clients: Assigning user ids

Before we approach access control, we need to refine our authentication system a bit. The simple setup used in the previous post did react to authentication requests, but did not differentiate between the connected clients. Luckily, socket.io does most of the work for us. Replace lines 29 and following of users.js with the following code:

plugins/users/users.js

  // User management API

  var api = {
    // get userdata. If a property is given as second argument, return just this property
    // the last argument is the callback
    getUserData : function( userid, arg2, arg3  ) {
      var property = arg3 ? arg2 : null;
      var callback = arg3 ? arg3 : arg2;
      userstore.get(userid, function( err, data ){
        if ( data )
        {
          if ( property ) return callback( null, data[property] );
          return callback( null, data );
        }
        return callback( "A user with id '"+userid+"' doesn't exist");
      });
    },
    // password authentication
    authenticate : function(userid, password, callback){
      userstore.get(userid, function( err, data ){
        // check password
        if ( data && data.password == password )
        {
          return callback(null,userid);
        }
        // authentication failed
        return callback( "Invalid username or password" );
      });
    }
  };

  // support of sessions

  // setup socket events
  var io = imports.socket;
  io.on("connection", function(socket){

    // helper functions using the API

    function login( data, callback )
    {
      api.authenticate( data.username, data.password, function(err,userid){
        if (err) return callback(err);
        socket.set("userid", userid, function(){
          console.log("User %s has logged in.", userid);
          // return user data to the client via the callback
          api.getUserData(userid, callback);
        });
      });
    }

    function logout( userid, callback )
    {
      socket.set("userid", null, function(){
        console.log("User %s has logged out.", userid);
        return callback();
      } );
    }

    // wire helpers to events

    socket.on("authenticate",function(data, callback){
      socket.get("userid", function(err,currentUserId){
        if( currentUserId )
        {
          return logout( currentUserId, function(){
            login( data, callback );
          });
        }
        login( data, callback );
      });
    });
    socket.on("logout", logout );
  });

  // register plugin and provide plugin API
  register(null,{
      users : api
  });

Note the difference between the user management “API”, which has no login our logout method because it has no way of knowing which client is currently connected. This information is only available to the socket.io session. We therefore will always need the “socket” object to know which user is attached to the object.

The ACL library

Unfortunately, as of now, not very many ACL libraries for node exist. The one library that uses commonly used terminology  (Users,Roles, Permissions, Resources) and seems to be best suited to our requirements  is node-acl (npm install acl), which is modeled on the Zend PHP Framework’s ACL library.

Since you have been following this tutorial, I won’t have to repeat how to set up the acl plugin. Here is t the main plugin file:

plugins/acl/acl.js

// This plugin provides access control
module.exports = function setup(options, imports, register)
{
  var acl = require("acl");
  acl = new acl(new acl.memoryBackend());

  // socket events
  var io = imports.socket;
  io.on("connection", function(socket){
    socket.on("allowedPermissions",function(resource,callback){
      socket.get("userid", function(err,userId){
        // anonymous has no permissions
        if( ! userId ) return callback(null,{});
        // for registered users, get permissions
        console.log("Querying permissions for "+userId+" on resource "+resource);
        acl.allowedPermissions( userId, resource, function(err,data){
          if(err) return callback(err);
          var permissions = data[resource];
          // prepare permission data for client consumption
          data = {};
          permissions.forEach(function(p){
            data[p] = true;
          });
          console.log(data);
          callback(null,data);
        });
      });
    });
  });

  // error checking callback
  var cb = function(err){
    if(err) console.log(err);
  }
  // create some mock permissions
  // in resource "db", users can only read, admins can read, write and delete
  acl.allow([{
    roles: 'admin',
    allows: [{
      resources: 'db',
      permissions: ['write', 'delete','read']
    }]
  },{
    roles: 'user',
    allows: [{
      resources: 'db',
      permissions: 'read'
    }]
  }],cb);

  // assing user ids to roles
  acl.addUserRoles("john","user",cb);
  acl.addUserRoles("mary","admin",cb);

  // register plugin and provide plugin API
  register(null,{
      acl : acl
  });
}

As you can see, we use Node ACL’s Memory backend to store roles, resouces and permissions. For more complex use cases, where changes to the ACL setup need to be persisted, the library also provides a Redis backend. More backends can easily be added. The code should be self-explanatory.

ACL on the client

As it is obvious from the above code, we expose only one method to the client (allowedPermissions), which takes a resource name as argument and returns permission data. The client only needs to concern itself with resources and permissions, it does not need to deal with users and roles. There is only one relevant user – the on that is currently logged in, and the server knows which one (through the socket object). There is no need to inform the client what roles the user has or which permissions is part of which role – that is all server-side data.

On the client, we need a resource controller object, which connects the permissions with UI states. Let’s have a look at the code. We need to make substantial changes and additions to our Application.js. I’ll document only the important parts, the whole file is on GitHub.:

testapp/source/class/testapp/Application.js

      // create the qx message bus singleton and give it a socket.io-like API
      // note that the argument passed to the subscriber is a qooxdoo event object
      var bus = qx.event.message.Bus.getInstance();
      bus.on = bus.subscribe;
      bus.emit = bus.dispatchByName;

      // set up socket.io
      var loc = document.location;
      var url = loc.protocol + "//" + loc.host + ":" + loc.port;
      var socket = io.connect(url + "/testapp");

      // Create a button
      var loginButton = new qx.ui.form.Button("Login", "testapp/test.png");
      var doc = this.getRoot();
      doc.add(loginButton, {left: 100, top: 50});

      // we'll need these vars in the closure
      var loginWindow, userid = null, username="";

      // Add an event listener for the button
      loginButton.addListener("execute", function(e)
      {
        // if someone is logged in, log out
        if (userid){
          return socket.emit("logout",userid, function(err){
            if(err) return alert("Something went wrong");
            loginButton.setLabel("Login");
            userid=null;
            bus.emit("updatePermissions");
          });
        }

        // create or reuse login window
        if ( ! loginWindow ){
          loginWindow = new dialog.Login({
            image : "dialog/logo.gif",
            text  : "Please log in",
            checkCredentials  : checkCredentials,
            callback : finalCallback
          });
        }
        loginWindow.show();
      },this);

      // this asyncronously checks the user credentials
      function checkCredentials( username, password, callback ) {
        socket.emit("authenticate", { username:username, password:password }, callback );
      }

      // this reacts on the result of the authentication
      function finalCallback(err, data){
        // error
        if (err) {
          return dialog.Dialog.error( err );
        }
        // Success!
        userid    = data.id;
        username  = data.name
        loginButton.setLabel( "Logout " + username );
        dialog.Dialog.alert("Welcome, " + username + "!" );
        // now permissions have changed, update them
        bus.emit("updatePermissions");
      }

As you can see, we have modified the login code a bit, so that username and userid are stored, and we use the qooxdoo message bus to inform listeners when the permissions should be updated (Changing the bus API is not really necessary, but I like the short “on” and “emit” better than the method names of the bus API).

Now we create a resource controller. This should really go into its own class, but we’re only interested in the main functionality of this controller.

      //  ACL

      // create a resource controller that reacts on permission updates
      // we don't do any type checking to keep this short
      function createController( resourceName ){
        var targets =[], permissions={};
        var controller = {
          // bind a property of a widget to a permission
          add : function( widget, property, permission, converter ){
            targets.push( {
              widget: widget,
              permission: permission,
              property: property,
              converter: converter || function(p){return p;}
            });
            return controller; // make it chainable
          },
          // set the permissions
          setPermissions : function(perms){
            permissions = perms;
          },
          // enforce the given or stored permissions with the controlled
          // widgets
          enforce : function(){
            targets.forEach(function(t){
              // compute new property value by calling hook function with
              // permission value and original property value
              var propVal = t.widget.get(t.property);
              var computedPropVal = t.converter(permissions[t.permission]||false, propVal);
              t.widget.set(t.property, computedPropVal );
            });
            return controller;
          },
          // pull the permissions from the server
          pull : function(){
            socket.emit("allowedPermissions",resourceName,function(err,data){
              if(err) return alert(err);
              controller.setPermissions(data);
              controller.enforce();
            });
            return controller;
          },
          // start listening to events concerning permissions and pull data
          start : function() {
            bus.on("updatePermissions", controller.pull );
            socket.on("updatePermissions", controller.pull );
            socket.on("acl-update-"+resourceName, controller.enforce );
            // this will normally disable everything since no permissions are set
            controller.enforce();
            // get permissions from server
            controller.pull();
            return controller;
          }
        };
        return controller;
      }

      // create new resource controller over a fictional "db" resource
      var dbController = createController("db");

      // create buttons
      var readButton = new qx.ui.form.Button("Read");
      doc.add(readButton, {left: 100, top: 100});
      var writeButton = new qx.ui.form.Button("Write");
      doc.add(writeButton, {left: 150, top: 100});
      var deleteButton = new qx.ui.form.Button("Delete");
      doc.add(deleteButton, {left: 200, top: 100});

      // delete button is only enabled when the checkbox is checked
      // a change in state needs to trigger an update
      var confirmDeleteCB = new qx.ui.form.CheckBox("Enable Delete");
      confirmDeleteCB.addListener("changeValue",dbController.enforce)
      doc.add(confirmDeleteCB,{left:270, top:100});

      // configure ACL
      dbController
        .add(readButton, /*property name*/ "enabled",/*permission name*/ "read")
        .add(writeButton, "enabled", "write")
        .add(deleteButton, "enabled", "delete", function(p,v){return p && confirmDeleteCB.getValue()})
        .add(confirmDeleteCB, "enabled", "delete")
        .add(confirmDeleteCB, "value", "delete", function(p,v){return p? v:false})
        .start();

The resource controller is very simple, but already quite powerful. It binds widget property values to permission states and observes changes in these states. It can be notified by other parts of the client application and then pulls the newest permission data from the server, or the server can push state changes to the server, using a socket.io message (“acl-update-RESOURCENAME”);

Since not all properties are boolean (as the permission values), a converter function can be used to transform a permission state into an appropriate property value. This converter allows to integrate additional logic that affects the UI: even though a user has a certain permission, the current state of the application might require that the permission cannot be given. In our example, the  “Delete” button is activated only if a checkbox is checked.

The converter function is called with two arguments, the permission state and the current value of the property. This allows, as the two next-to-last lines in the code above shows, to implement the following logic: if the “delete” permission is granted, enable the checkbox, and preserve the current checkbox state (checked/unchecked), otherwise disable and uncheck it.

If you build and start the application now (cd testapp; python ./generate.py –no-progress-indicator build; cd ..; node server.js build; http://<projectname&gt;.<username>.c9.io). you’ll see three buttons below the login button. They are disabled when no user is logged in. If you log in with john/john, only the “read” button is enabled, as John is only a regular user. However, if you log in Mary (with mary/mary), an “admin” role, all buttons exept the “delete” button are enabled, as is the “confirm delete” checkbox. When you check the confirmation checkbox, the “delete” button is enabled. When you log out, all buttons will be immediately disabled.

(Update: you can also access the application on nodejitsu).

Even though this is, again, very basic stuff and doesn’t do much, the underlying logic covers already a lot of use cases concerning the control of UI elements. I would be interested, however, on your views on the general setup. What level of complexity is missing which is necessary for production-grade applications?

Advertisements

One thought on “Developing a complete client-server application with qooxdoo and NodeJS on Cloud9IDE. Part 4: Access Control [Updated]

  1. Probably a better name for the resource controller’s “enforce” method would be “updateGui”. I didn’t want to call it “update”, since that might be confused with the “pull” method, which updates the permission states from the server.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s