Contents


Use business rules as an authorization engine

Comments

In this article, you learn how to use the Nools business rules engine to make authorization decisions in a Node.js application. Doing so enables you to change the authorization policy of an application without making changes to the source code, making it much easier to keep such a policy up to date.

What you'll need to build your application

  • A Bluemix account
  • Knowledge of HTML, JavaScript, and the MEAN web application stack
  • A development environment that can upload a Node.js application to Bluemix, such as Eclipse

Versions of the application

There are two demonstration versions of this application. The first, which shows the application with hard-wired authorization, shows the original security policy. The second, which shows the application after the introduction of business-rule-based authorization, illustrates a much more flexible security policy. Note that the security policy of the second application is a shared resource, so you cannot be sure it would be the same as the one shown in the early sections of the article.

Run the original app Run the modified app

In this article, I show you how to implement the policies of your Node.js application as a rule base, and how to provide a user interface to that rule base. Changes to your authorization policy are thereby made much simpler and require no programmer involvement.

The demonstration application

The rule base functionality of my application is demonstrated using what I call the "World's Silliest Bank." The bank's system is available through the Internet, for use by bank tellers and customers. Users are trusted to select their identity from a menu at the top of the browser window. The browser then sends a REST request to the server to get the list of accounts and account balances to display. The server decides on the list of accounts that should be visible to the user, whether the balances on them should be visible, and sends back a response with the information the user may see. The browser displays that information.

Currently, the authorize function returns true in two cases:

  • If the role of the subject (the application user) is Teller and the subject's branch is the same as the branch of the object (the account). This lets tellers see all the accounts in their branch and tell customers their balances.
  • If the role of the subject is Customer and the subject is the same as the object. This lets customers see their own balances.
var authorizeAction = function(subject, verb, object) {
  // Get additional information
  var subjectInfo = users[subject];
  var objectInfo = users[object];

  // Let customers see their own balance, and tellers
  // the balances of everybody in their branch.
  if (subjectInfo.role == "Teller")
    return subjectInfo.branch == objectInfo.branch;
  else if (subjectInfo.role == "Customer")
    return subject == object;

  // If no rule allows access, deny it.
  return false;
};

The authorize function ignores the verb, because the application has only one verb, to show the account balance.

Step 1. Start using the Nools rule engine

Because the policy being implemented in this example is very simple, I chose to use Nools, which appears to be the most popular rule engine for Node.js (rule engines are typically implemented as libraries, which makes them language specific). You can learn more about this rule engine at the Nools page.

  1. To have the rule engine available, add "nools": "*" to the dependencies section of the package.json file.
  2. Add the following code to the app.js file to use the rule engine. At this point, it does not actually do anything.
    // Use the Nools library
    var nools = require('nools');
    
    // Create a new flow, a rule base.
    // For now, keep the rule base empty.
    var flow = nools.flow("authz", function(flow) {
      ;
    });
    
    // Create a new session. A session combines
    // a rule base with facts to arrive at a decision.
    var session = flow.getSession();
    
    // Add facts to the session
    session.assert("Hello");
    session.assert("Goodbye");
    
    // Attempt to use the rule base
    session.match().then(
      function() {
        console.log("Successfully ran the flow");
      },
      function(err) {
        console.log("Error" + err);
      }
    );
    
    // Dispose of the session, delete all the facts to make it
    // usable in the future
    session.dispose();
  3. Push the application (upload and run it).
  4. View the log file. If you are using Eclipse, it is in the console window. If you use the cf command-line interface, run this command:
    cf logs <application name> --recent
  5. Verify that you see the success message (Success in running the flow).

Step 2. Create object classes

The facts that Nools uses, either to evaluate rules or as conclusions, can be stored in objects. For authorization decisions, it is easiest to have two classes:

  • AuthzRequest for the request information (including any relevant additional information)
  • AuthzResponse for the response

Add the following code to the app.js file, putting it before the code from Step 1.

// Object class for authorization request
var AuthzRequest = function(subject, verb, object) {
  this.subect = subject;
  this.verb = verb;
  this.object = object;
};


// Object class for authorization response
var AuthzResponse = function(answer) {
  this.answer = answer;
}

The object classes are defined using the standard JavaScript mechanism, a constructor that fills up the necessary fields.

Step 3. Create a permissive flow

For this step, create a permissive flow and fire it.

  1. Modify the definition of the flow variable to add a permissive rule to the flow:
    // Create a new flow, a rule base.
    // Be permissive
    var flow = nools.flow("authz", function(flow) {
    
      this.rule("Permissive", // Rule name
    
      // The facts on which the rule operates. If the list
      // for a fact has two items, the first is the object class
      // and the second is the variable name. If it has three, the
      // the third is a condition that has to evaluate to true for the
      // rule to be applied.
      //
      // When there is only one fact, it can be in an un-nested list,
      // the nested list here is just for illustration of the general
      // case with multiple facts.
        [[AuthzRequest, "req"]],
    
    
        // The function to call if the rule is fulfilled
        function(facts) {
    
          // The parameter contains the facts.
          
          // Prove we got the parameter
          console.log(facts.req.verb);
          
          // Always allow
          this.assert(new AuthzResponse(true));
        }
      );
    });
  2. Replace the two existing calls to session.assert with one that includes an AuthzRequest.
    // Add an AuthzRequest fact to the session
    session.assert(new AuthzRequest("subject", "verb", "object"));
  3. After the session.match call, add a request to read the result:
    console.log(session.getFacts(AuthzResponse));
  4. Push the code and view the log. Verify that you get a line similar to this one:
    2015-05-27T20:20:50.64-0500 [App/0]      OUT [ { answer: true } ]

Step 4. Modify the authorizeAction function to call the rule engine

  1. Remove all the calls in the app.js file that refer to the session variable. They were there to help with learning about the rule engine and are no longer needed.
  2. Replace the authorizeAction function with this:
    // The function that actually authorizes a user (subject)
    // to do something, such as view the balance (verb)
    // of an account (object).
    var authorizeAction = function(subject, verb, object) {
      // Get additional information
      var subjectInfo = users[subject];
      var objectInfo = users[object];
    
      // Add the names to the information to make it easier
      // to use the rule base.
      subjectInfo.name = subject;
      objectInfo.name = object;
    
      // Create a new session. A session combines
      // a rule base with facts to arrive at a decision.
      var session = flow.getSession();
    
      // Add an AuthzRequest fact to the session
      session.assert(new AuthzRequest(subjectInfo, verb, objectInfo));
    
      // Call the flow for a decision
      session.match().then(
        function() {
          console.log("Successfully ran the flow");
        },
        function(err) {
          console.log("Error" + err);
        }
      );
    
      // Get the decision. session.getFacts(<type>) gets all the
      // facts of that type. In this case, there would be one
      // AuthzResponse.
      var resultList = session.getFacts(AuthzResponse);
      var decision;
    
      if (resultList.length == 0)
        // There would be no AuthzResponse if no rule triggered.
        // If no rule permits an action, it is denied.
        decision = false;
      else
        decision = resultList[0].answer;
    
      // Dispose of the session, delete all the facts to make it
      // usable in the future
      session.dispose();
    
      return decision;
    };
  3. Push the application.
  4. Use the application. Ensure that you are able to view all of the accounts, regardless of the user you choose.

Step 5. Return to the original policy

To return to the original policy, replace the nools.flow call with one that has the rules in the policy:

// Create a new flow, a rule base.
var flow = nools.flow("authz", function(flow) {

  this.rule("Teller", // Rule name

  // Notice the added third member of the list, to restrict
  // this rule to cases where the subject is a teller.
    [[AuthzRequest, "req", "req.subject.role=='Teller'"]],


    // The function to call if the rule is fulfilled
    function(facts) {
      // Allow if the subject and object share the
      // same branch.
      this.assert(new AuthzResponse(
        facts.req.subject.branch == facts.req.object.branch));
    }
  );

  this.rule("Customer", // Rule name

  // Notice the added third member of the list, to restrict
  // this rule to cases where the subject is a customer.
    [[AuthzRequest, "req", "req.subject.role=='Customer'"]],


    // The function to call if the rule is fulfilled
    function(facts) {
      // Allow if the subject and object have the same name,
      // let the customer see his/her own balance.
      this.assert(new AuthzResponse(
        facts.req.subject.name == facts.req.object.name));
    }
  );

});

Step 6. Store the policy in an object

So far, you have replaced a few simple lines of code with much longer code that does the same thing. However, creating needlessly obtuse code is not really our purpose. The purpose is to create a policy that people will be able to edit without changing the source code.

There are two ways to accomplish this. The first is to use Nools' own DSL (domain specific language). However, that language is very flexible and as a result, very complicated. It does not fit the purpose of making it possible for non-programmers to change the policy.

The second way is to put the entire policy in a JavaScript object, and then provide a user interface to modify that object. This requires the same type of programming expertise required for the application itself.

There are multiple ways in which rules can be expressed. In the case of this particular application, an authorization request always has the same six variables:

  • subject.name
  • subject.role
  • subject.branch
  • object.name
  • object.role
  • object.branch

Because there are so few variables, the easiest way to specify rules is to specify the variable values required for an action to be authorized. Those values can be a constant (for example, subject.role equal to Teller), another variable (subject.branch equal to object.branch), or a special value to mean that particular variable doesn't matter in that rule.

  1. Add an object for the security policy:
    // Security policy. The policy includes three parameters:
    //
    // Vars is the variables that make up the policy.
    // Constants are the constants that may appear in the policy.
    // (note, these are only required for the user interface)
    //
    // Rules are the actual rules. Each rule contains variables
    // (enclosed in quotes to allow for dots within a variable name)
    // and the values they need to match. They can be matched against
    // constants or other variables. If the value of a variable does
    // not matter for the rule, it does not appear in that rule.
    //
    // The rules are all permits. If a request does not match any rules,
    // it is denied.
    var secPolicy = {
      vars: ["subject.name", "subject.role", "subject.branch",
              "object.name", "object.role", "object.branch" ],
      constants: ["Teller", "Customer", "Austin", "Boston"],
      rules: [
        {   // The teller rule
          "subject.role": {type: "constant", value: "Teller"},
          "subject.branch": {type: "variable", value: "object.branch"}
        },
        {  // The customer rule
          "subject.role": {type: "constant", value: "Customer"},
          "subject.name": {type: "variable", value: "object.name"}
        }
      ]
    };
  2. Processing the rules is sufficiently complicated that having access to external functions helps. Unfortunately, inside a Nools pattern you have access only to facts. So, to place functions into facts, create a function object class:
    // Object class for functions, so they will be
    // usable as "facts" within Nools
    var FunObj = function(name, fun) {
      this.name = name;
      this.fun = fun;
    }
  3. In the authorizeAction function, assert a new fact with the matchRuleRequest function:
    // Add a necessary function as a "fact"
      session.assert(new FunObj("matchRuleRequest", matchRuleRequest));
  4. Add the actual matchRuleRequest function and a utility function it uses:
    // Get a value in a request from a rule style variable name
    var getRequest = function(request, varName) {
      // The outer and inner variable names in the request
      // The rule has rule["subject.role"],
      // but the AuthorizationRequest has
      // request["subject"]["role"] for that
      reqVarNames = varName.split(".");
    
      // The value in the request value
      return request[reqVarNames[0]][reqVarNames[1]];
    }
    
    
    // Check if an authorization request matches a rule
    var matchRuleRequest = function(ruleNumber, request) {
      var rule = secPolicy.rules[ruleNumber];
    
      for (variable in rule) {
        // Get the value
        var ruleValue = rule[variable];
    
        // If it is a constant, check equality to that constant
        if (ruleValue.type == "constant" &&
          ruleValue.value != getRequest(request, variable))
            return false;
    
        // If it is a variable, get the value in that variable
        // and compare
        if (ruleValue.type == "variable" &&
          getRequest(request, ruleValue.value)
          != getRequest(request, variable))
            return false;
      }
    
      // If we get here then there are no mismatches.
      return true;
    };
  5. Modify the function that creates the flow to use the policy.
    // Create a new flow, a rule base.
    var flow = nools.flow("authz", function(flow) {
    
      // Create rules from the policy
      for(var i=0; i<secPolicy.rules.length; i++) {
        this.rule("Rule #" + i,   // Rule name
          [
            // Find two facts, each with a pattern. The first fact,
            // match, just makes the matchRuleRequest function available
            // if the context of the matching pattern for the second
            // function, which checks if an rule matches the 
            // authorization request.
            [FunObj, "match", "match.name == 'matchRuleRequest'"],
            [AuthzRequest, "req", "match.fun(" + i + ", req)"]
          ],
          function(facts) {
            // If we get here, the rule matches, so allow
            this.assert(new AuthzResponse(true));
          }
        );
      }
    });
  6. Push and verify that the application still follows the security policy.

Step 7. Create a user interface for the policy

Finally, to allow administrators who are not programmers to modify the policy, you need to create a user interface for it. You can see the files for the security policy interface, public/policy.html and public/scripts/policy.js, in the source code. They use Angular in a fairly standard way, so I am not going to get into them too deeply. To learn more about Angular, see the "Build a self-posting Facebook application with Bluemix and the MEAN stack" series here on developerWorks.

One interesting point is that the HTML select tag works best with string values. Therefore, instead of identifying parameter values as objects (for example, {type: "constant", value: "Teller"}), the policy on the browser uses strings with a prefix that determines if the value is a variable or a constant (for example, c:Teller). The policy is transferred using REST, whose use is also explained in the "Build a self-posting Facebook application with Bluemix and the MEAN stack" series.

Another issue is that the demonstration application does not save the policy anywhere; it just keeps it in memory. This means that the policy is lost when the application is restarted. A real-world application would normally have access to a database, such as MongoDB, and store the policy there. Using MongoDB is also explained in the series just mentioned.

You can now use the final application at http://world-silliest-bank-after.mybluemix.net/. Just note that the security policy is global, so if it seems to change randomly it could be because somebody else is changing it at the same time. You have a button to download the current security policy in the policy.html file to check for that possibility.

Conclusion

In this article, to focus on the important topic, the authorization engine, I have used a very simple application, which, therefore, has a very simple security policy. A rule engine such as Nools is overkill for such a simple application and security policy. However, real applications are considerably more complicated. Using the business rules approach, the authorization engine can gather additional information (as facts) from third-party servers and check if values are higher than a threshold or if users are members of particular groups.

To get started with a real application, consider the authorization needs:

  • Who are the users—the subjects? What information about a user could possibly be relevant to authorization decisions?
  • What are the actions that need to be authorized—the verbs?
  • For each of those actions, what is the type of object it affects? What attributes of those objects could be relevant to authorization decisions?

After you identify the information that would be needed to make decisions and how to obtain it, the next step is to figure out the user interface. Do you just need to check variables for equality (as in this article)? Do you need to check if a variable is a member of a group, or above or below a threshold? How would you present the various options in a way that non-technical users could understand so they would be able to change the authorization policy without programmer involvement?


Downloadable resources


Related topics


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Security, Cloud computing
ArticleID=1008060
ArticleTitle=Use business rules as an authorization engine
publish-date=06162015