内容


使用业务规则作为授权引擎

Comments

在本文中,您将学习如何使用 Nools 业务规则引擎在 Node.js 应用程序中制定授权决策。通过这种方式,您可以在无需更改源代码的情况下更改应用程序的授权策略,让保持策略最新变得非常容易。

构建您的应用程序需要做的准备工作

  • 一个 Bluemix 帐户
  • HTML、JavaScript 和 MEAN Web 应用程序堆栈的知识
  • 一个可将 Node.js 应用程序上传到 Bluemix 的开发环境,比如 Eclipse

应用程序的版本

此应用程序有两个演示版本。第一个显示了原始的安全策略,即具有硬编码授权的应用程序。第二个演示了一种灵活得多的安全策略,即引入了基于业务规则的授权后的应用程序。请注意,第二个应用程序的安全策略是一种共享资源,所以无法确保它与本文前几节中给出的策略相同。

运行原始应用程序 运行修改后的应用程序

在本文中,我将展示如何将 Node.js 应用程序的策略实现为规则库,以及如何为该规则库提供一个用户界面。这样,更改授权策略就变得简单得多,无需程序员干预。

演示应用程序

我的应用程序的规则库功能使用了 “World's Silliest Bank” 来演示。该银行的系统可通过互联网获得,可供银行出纳和客户使用。用户诚实地从浏览器窗口顶部的菜单中选择他们的身份。然后,浏览器会向服务器发送一个 REST 请求,获取要显示的帐户列表和帐户余额。由服务器决定用户可以看到的帐户列表,以及帐户上的余额是否可见,并发送一个包含用户可以看到的信息的响应。浏览器显示了该信息。

目前,authorize 函数会在两种情况下返回 true:

  • 如果主体(应用程序用户)的职位是出纳,而且主体的支行与对象(帐户)的支行相同。这让出纳可以看到其支行内的所有帐户,并告诉客户的余额。
  • 如果主体的角色是客户,而且主体与对象相同。这让客户可以看到自己的余额。
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;
};

authorize 函数会忽略动词,因为应用程序只有一个洞察来显示帐户余额。

第 1 步. 开始使用 Nools 规则引擎

因为本例中实现的策略非常简单,所以我选择使用 Nools,它似乎是 Node.js 的最流行的规则引擎(规则引擎通常被实现为库,这使它们能够特定于语言)。您可以在 Nools 页面 上了解此规则引擎的更多信息。

  1. 要让该规则引擎可用,需要将 "nools":"*" 添加到 package.json 文件的 dependencies 部分。
  2. 将以下代码添加到 app.js 文件中,以便使用该规则引擎。目前它不会执行任何实际操作。
    // 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. 推送应用程序(上传并运行它)。
  4. 查看日志文件。如果使用 Eclipse,那么日志文件位于控制台窗口中。如果使用 cf 命令行接口,可以运行以下命令:
    cf logs <application name> --recent
  5. 确认您看到了成功消息 (Success in running the flow)。

第 2 步. 创建对象类

Nools 使用的事实(无论是用于评估规则,还是用作结论)可存储在对象中。对于授权决策,在最简单的情形下,拥有两个类:

  • AuthzRequest 用于请求信息(包括任何相关的附加信息)
  • AuthzResponse 用于响应

将以下代码添加到 app.js 文件中,放在 第 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;
}

对象类是使用标准 JavaScript 机制来定义的,也就是说,它使用了一个已填充必要字段的构造函数。

第 3 步. 创建许可流

这一步是创建一个许可流并触发它。

  1. 修改 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. 将对 session.assert 的两处现有的调用替换为一个包含 AuthzRequest 的调用。
    // Add an AuthzRequest fact to the session
    session.assert(new AuthzRequest("subject", "verb", "object"));
  3. session.match 调用后,添加一个读取结果的请求:
    console.log(session.getFacts(AuthzResponse));
  4. 推送代码并查看日志。确认您获得了与以下内容类似的一行信息:
    2015-05-27T20:20:50.64-0500 [App/0]      OUT [ { answer: true } ]

第 4 步. 修改 authorizeAction 函数来调用规则引擎

  1. 删除 app.js 文件中所有引用 session 变量的调用。它们只是为了帮助学习规则引擎,现在已不需要。
  2. 将 authorizeAction 函数替换为:
    // 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. 推送应用程序。
  4. 使用应用程序。确保您能够查看所有帐户,无论您选择哪个用户。

第 5 步. 返回到原始策略

要返回到原始策略,可将 nools.flow 调用替换为拥有该策略中的规则的调用:

// 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));
    }
  );

});

第 6 步. 将策略存储在对象中

目前位置,您将一些简单的代码行替换为了执行相同操作但复杂得多的代码。但是,创建过于迟缓的代码不是我们的真正目的。我们的目的是这样一个策略,人们无需更改源代码即可编辑它。

可通过两种方式实现此目的。第一种是使用 Nools 自己的 DSL(特定于域的语言)。但是,该语言非常灵活,因此也非常复杂。它不适合让非程序员能够更改策略的目的。

第二种方式是将整个策略放在 JavaScript 对象中,然后提供一个用户界面来修改该对象。这需要掌握与应用程序本身同类的编程经验。

可通过多种方式来表达规则。对于这个特定的应用程序,授权请求始终包含相同的 6 个变量:

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

因为变量非常少,所以指定规则的最简单方式是指定授权一个操作所需的变量值。这些值可以是常量(例如 subject.role 等于 Teller)、另一个变量(subject.branch 等于 object.branch),或者一个表示特定变量在该规则中无关紧要的特殊值。

  1. 为安全策略添加一个对象:
    // 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. 规则的处理非常复杂,有必要请求外部函数帮助。不幸的是,在 Nools 模式中,您仅能访问事实。所以,要将函数放在事实中,可以创建一个函数对象类:
    // 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. authorizeAction 函数中,使用 matchRuleRequest 函数断言一个新事实:
    // Add a necessary function as a "fact"
      session.assert(new FunObj("matchRuleRequest", matchRuleRequest));
  4. 添加实际的 matchRuleRequest 函数和它使用的一个效用函数:
    // 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. 修改创建该流的函数以使用该策略。
    // 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. 推送并确认应用程序仍遵守安全策略。

第 7 步. 为策略创建一个用户界面

最后,要使得不是程序员的管理员也能够修改策略,需要为策略创建一个用户界面。可以在源代码中看到安全策略接口的文件 public/policy.html 和 public/scripts/policy.js。它们以一种非常标准的方式使用 Angular,所以我不会太深入地介绍它们。要了解 Angular 的更多信息,请参阅 developerWorks 上的 “使用 Bluemix 和 MEAN 堆栈构建自动发表 Facebook 应用程序” 系列。

一个有趣的地方是,HTML select 标记非常适合字符串值。因此,无需将参数值识别为对象(例如 {type:"constant", value:"Teller"}),浏览器上的策略使用具有前缀的字符串,该前缀确定了该值是变量还是常量(例如 c:Teller)。该策略使用了 REST 传输,REST 的使用也已在 “使用 Bluemix 和 MEAN 堆栈构建自动发表 Facebook 应用程序” 系列中介绍过。

另一个问题是,演示应用程序没有将策略保存在任何地方;而是只将它保留在内存中。这意味着重新启动应用程序时策略将丢失。真实的应用程序通常能够访问一个数据库,比如 MongoDB,并将策略存储在那里。MongoDB 的使用也已在刚提及的系列中介绍过。

您现在可以在 http://world-silliest-bank-after.mybluemix.net/ 上使用最终的应用程序。请注意,安全策略是全局性的,所以如果它看起来像是在随机变化,那么可能是因为其他人同时在更改它。您有一个按钮来下载 policy.html 文件中的当前安全策略,以查看是否存在这种可能性。

结束语

在本文中,为了重点介绍授权引擎这个重要主题,我使用了一个非常简单的应用程序,因此这个应用程序具有非常简单的安全策略。对于这样一个简单的应用程序和安全策略,Nools 等规则引擎有点大材小用。但是,真实的应用程序要复杂得多。通过使用业务规则方法,授权引擎可从第三方服务器收集更多的信息(作为事实),检查某个值是否高于阈值,或用户是否是特定组的成员。

要开始创建真实的应用程序,请考虑授权要求:

  • 谁是用户 — 主体?哪些用户信息可能与授权决策相关?
  • 需要授权哪些操作 — 动词?
  • 对于每种操作,它会影响何种类型的对象?这些对象的哪些属性可能与授权决策相关?

确定了做出决策所需的信息和如何获取它之后,下一步是设计用户界面。您是否只需要检查变量的质量(像本文中这样)?您是否需要检查一个变量是否是某个组的成员,或者是高于还是低于一个阈值?您如何以非技术用户能够理解的方式呈现各种选项,让他们无需程序员干预即可更改授权策略?


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Cloud computing, Security
ArticleID=1012004
ArticleTitle=使用业务规则作为授权引擎
publish-date=07282015