在本文中,您将学习如何使用Nools业务规则引擎在Node.js应用程序中做出授权决策。 这样做使您可以更改应用程序的授权策略,而无需更改源代码,从而使更新此类策略更容易。
构建应用程序所需的条件
- 一个Bluemix帐户
- HTML,JavaScript和MEAN Web应用程序堆栈知识
- 可以将Node.js应用程序上载到Bluemix的开发环境,例如Eclipse
应用程序的版本
此应用程序有两个演示版本。 第一个显示具有硬连线授权的应用程序,显示原始的安全策略。 第二部分显示了引入基于业务规则的授权之后的应用程序,该示例说明了更加灵活的安全策略。 请注意,第二个应用程序的安全策略是共享资源,因此无法确定它是否与本文前面的部分相同。
“在本文中,我将向您展示如何将Node.js应用程序的策略实现为规则库,以及如何为该规则库提供用户界面。 因此,对授权策略的更改变得非常简单,并且不需要程序员参与。 ”
示范应用
我所谓的“世界上最简单的银行”展示了我的应用程序的基于规则的功能。 该银行的系统可通过Internet使用,供银行出纳员和客户使用。 信任用户可以从浏览器窗口顶部的菜单中选择其身份。 然后,浏览器将REST请求发送到服务器,以获取要显示的帐户列表和帐户余额。 服务器决定对用户应该可见的帐户列表,是否应显示这些帐户上的余额,然后将响应与用户可能看到的信息一起发送回去。 浏览器显示该信息。
当前,在两种情况下,authorize函数返回true:
- 如果主题(应用程序用户)的角色是Teller,并且主题的分支与对象(帐户)的分支相同。 这样,柜员就可以查看其分支机构中的所有帐户并告诉客户其余额。
- 如果主题的角色是客户,并且主题与对象相同。 这使客户可以查看自己的余额。
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页面上了解有关此规则引擎的更多信息。
- 要使规则引擎可用,请在package.json文件的
dependencies
部分添加"nools": "*"
。 - 将以下代码添加到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();
- 推送应用程序(上载并运行)。
- 查看日志文件。 如果使用的是Eclipse,则在控制台窗口中。 如果使用
cf
命令行界面,请运行以下命令:cf logs <application name> --recent
- 验证您是否看到成功消息(
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机制定义对象类,JavaScript机制是一种填充必要字段的构造函数。
步骤3.创建一个许可流
对于此步骤,创建许可流并将其激发。
- 修改
flow
变量的定义以向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)); } ); });
- 将两个对
session.assert
现有调用替换为包含AuthzRequest
一个。// Add an AuthzRequest fact to the session session.assert(new AuthzRequest("subject", "verb", "object"));
- 在
session.match
调用之后,添加一个请求以读取结果:console.log(session.getFacts(AuthzResponse));
- 推送代码并查看日志。 验证您是否获得了与此类似的一行:
2015-05-27T20:20:50.64-0500 [App/0] OUT [ { answer: true } ]
步骤4.修改authorizeAction
函数以调用规则引擎
- 删除app.js文件中所有引用
session
变量的调用。 他们在那里是为了帮助您了解规则引擎,并且不再需要它们。 - 用以下代码替换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; };
- 推送应用程序。
- 使用该应用程序。 确保无论您选择哪个用户,您都可以查看所有帐户。
步骤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对象中,然后提供用于修改该对象的用户界面。 这需要应用程序本身所需的相同类型的编程专业知识。
有多种表达规则的方式。 对于此特定应用程序,授权请求始终具有相同的六个变量:
-
subject.name
-
subject.role
-
subject.branch
-
object.name
-
object.role
-
object.branch
因为变量很少,所以指定规则的最简单方法是指定授权操作所需的变量值。 这些值可以是常数(例如, subject.role
等于Teller
),另一个变量( subject.branch
等于object.branch
),或一个特殊的值意味着特定变量无所谓在该规则。
- 为安全策略添加一个对象:
// 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"} } ] };
- 处理规则非常复杂,因此可以使用外部功能。 不幸的是,在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; }
- 在
authorizeAction
函数中,使用matchRuleRequest
函数声明一个新事实:// Add a necessary function as a "fact" session.assert(new FunObj("matchRuleRequest", matchRuleRequest));
- 添加实际的
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; };
- 修改创建流程以使用策略的功能。
// 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)); } ); } });
- 推送并验证应用程序是否仍然遵循安全策略。
步骤7.为策略创建用户界面
最后,要允许不是程序员的管理员修改策略,您需要为其创建用户界面。 您可以在源代码中看到安全策略界面的文件public / policy.html和public / scripts / policy.js。 他们以相当标准的方式使用Angular,因此我不会太深入地研究它们。 要了解有关Angular的更多信息,请参阅developerWorks上的“ 使用Bluemix和MEAN堆栈构建自发布Facebook应用程序 ”系列。
有趣的一点是HTML select
标记最适合字符串值。 因此,浏览器上的策略不是将参数值标识为对象(例如{type: "constant", value: "Teller"}
),而是使用带有前缀的字符串来确定该值是变量还是常量(例如c:Teller
)。 该策略是使用REST传输的,在“使用Bluemix和MEAN堆栈构建自发布Facebook应用程序”系列中也对REST的用法进行了说明。
另一个问题是演示应用程序无法将策略保存在任何地方。 它只是将其保存在内存中。 这意味着重新启动应用程序时该策略将丢失。 实际应用程序通常可以访问数据库(例如MongoDB),并将策略存储在该数据库中。 刚刚提到的系列还介绍了如何使用MongoDB。
您现在可以在http://world-silliest-bank-after.mybluemix.net/上使用最终应用程序。 请注意,安全策略是全局的,因此,如果它似乎是随机更改的,则可能是因为其他人正在同时更改它。 您有一个按钮可以下载policy.html文件中的当前安全策略以检查这种可能性。
结论
在本文中,为了重点关注授权引擎这一重要主题,我使用了一个非常简单的应用程序,因此它具有非常简单的安全策略。 对于如此简单的应用程序和安全策略,诸如Nools之类的规则引擎实在是太过分了。 但是,实际的应用程序要复杂得多。 使用业务规则方法,授权引擎可以从第三方服务器收集其他信息(事实),并检查值是否高于阈值或用户是否为特定组的成员。
要开始使用真实的应用程序,请考虑授权需求:
- 谁是使用者?使用者? 关于用户的哪些信息可能与授权决策有关?
- 动词需要授权哪些动作?
- 对于这些操作中的每一个,它影响的对象类型是什么? 这些对象的哪些属性可能与授权决策有关?
在确定了决策所需的信息以及如何获取信息之后,下一步就是弄清楚用户界面。 您是否只需要检查变量是否相等(如本文所述)? 您是否需要检查变量是否是组的成员,还是高于或低于阈值? 您将如何以非技术用户可以理解的方式展示各种选项,以便他们无需程序员参与即可更改授权策略?