目次


ビジネス・ルールをアクセス許可エンジンとして使用する

Comments

この記事では、Nools ビジネス・ルール・エンジンを使用して Node.js アプリケーションでのアクセス許可を決定する方法を説明します。Nools ビジネス・ルール・エンジンを使用すれば、ソース・コードに手を加えることなくアプリケーションのアクセス許可ポリシーを変更することができ、極めて容易にポリシーを最新の状態に維持することができます。

アプリケーションを作成するために必要となるもの

  • Bluemix アカウント
  • HTML、JavaScript、および MEAN Web アプリケーション・スタックの知識
  • Node.js アプリケーションを Bluemix にアップロードできる開発環境 (Eclipse など)

アプリケーションのバージョンについて

このアプリケーションには 2 つのデモ・バージョンがあります。1 つは、元のセキュリティー・ポリシーがわかるように、アクセス許可をハード・コーディングしたアプリケーションです。もう 1 つは、ビジネス・ルール・ベースのアクセス許可を導入したアプリケーションで、セキュリティー・ポリシーを遥かに柔軟に設定できるようになっています。後者のアプリケーションのセキュリティー・ポリシーは共有リソースであるため、記事の前のほうのセクションで説明したときのポリシーと同じポリシーになっているとは限らないことに注意してください。

元のアプリを実行する変更後のアプリを実行する

この記事では、Node.js アプリケーションのポリシーをルール・ベースとして実装する方法、そしてそのルール・ベースを操作するユーザー・インターフェースを提供する方法を説明します。ポリシーをルール・ベースにすることで、アクセス許可ポリシーを変更するのが遥かに簡単になり、プログラマーが関与しなくても済むようになります。

デモ・アプリケーション

ルール・ベースの機能をデモするために、「World's Silliest Bank」と名付けたサンプル・アプリケーションを使用します。この銀行のシステムは、銀行の出納係と顧客がインターネットからアクセスして利用することができます。ユーザーは自分の身元を、ブラウザー・ウィンドウの上部メニューから選択します。すると、ブラウザーは表示する口座とその残高のリストを取得するための REST リクエストをサーバーに送信します。サーバーは、ユーザーに表示する口座のリスト、そしてそれらの口座の残高を表示可能にすべきかどうかを決定してから、ユーザーに表示を許可する情報をレスポンスで返します。その情報が、ブラウザーに表示されます。

アクセス許可関数は、現時点では以下の条件で true を返します。

  • 主語 (subject) であるアプリケーション・ユーザーのロール (role) が出納係 (Teller) であり、主語の支店 (branch) が、目的語 (object) である口座の支店と同じであること。この場合、出納係は所属する支店のあらゆる口座を確認して、顧客に口座残高を通知することができます。
  • 主語のロールが顧客 (Customer) であり、主語と目的語が同じであること。この場合、顧客は自分の口座残高を表示することができます。
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;
};

アクセス許可関数は、動詞を無視します。このアプリケーションには、口座残高を「表示する」という 1 つの動詞 (verb) しかないためです。

ステップ 1. Nools ルール・エンジンを導入する

この例で実装するポリシーはかなり単純なものなので、Nools を使用することにしました。Nools は Node.js で最もよく使われているルール・エンジンのようです (ルール・エンジンは一般にライブラリーとして実装されるため、特定の言語に固有のものになっています)。このルール・エンジンの詳細については、Nools のページを参照してください。

  1. ルール・エンジンを使用可能にするために、package.json ファイルの dependencies セクションに "nools":"*" を追加します。
  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 がルールを評価するために使用したり、結論として使用したりするファクトは、オブジェクトに格納することができます。アクセス許可を決定するためには、以下の 2 つのクラスを使用するのが最も簡単です。

  • 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 に対する既存の 2 つの呼び出しを、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. ポリシーをオブジェクトに格納する

これまでのところでは、単純な数行のコードを、最終的な実行結果が同じになる、遥かに長いコードで置き換えてきました。ただし実際には、不必要に長いコードを作成することを目的としているわけではありません。目的は、ユーザーがソース・コードに手を加えることなく編集可能なポリシーを作成することです。

この目的を達成するには、2 つの方法があります。その 1 つは、Nools 独自の DSL (ドメイン特化言語) を使用することです。けれども、この言語は非常に柔軟であるため、結果として非常に複雑になってしまいます。それでは、プログラマーでなくてもポリシーを変更できるようにするという目的には適いません。

もう 1 つの方法は、ポリシー全体を JavaScript オブジェクトに格納し、そのオブジェクトを変更するためのユーザー・インターフェースを提供することです。その場合に必要となるプログラミングの専門知識は、このアプリケーション自体に必要な専門知識とほとんど変わりません。

ルールを表現するには複数の方法があります。この特定のアプリケーションの場合、アクセス許可リクエストには常に同じ変数が含まれます。それらの変数は、以下の 6 つです。

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

変数はこれしかないため、アクションを許可するために必要な変数の値を指定することが、ルールを指定する最も簡単な方法となります。これらの変数の値は、定数にすることや (例えば、subject.roleTeller にするなど)、別の変数にすること (subject.branchobject.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 を使用する方法についても、連載「Bluemix と MEAN スタックを使用して自動投稿 Facebook アプリケーションを作成する」で説明しています。

1 つの問題として、このデモ・アプリケーションでは、ポリシーはどこにも保存されず、メモリー内に保持されるだけなので、アプリケーションを再起動すると、ポリシーは失われることになります。実際のアプリケーションでは、MongoDB などのデータベースにアクセスして、そこにポリシーを格納するのが通常です。MongoDB の使用方法についても、上述の連載で説明しています。

最終的なアプリケーションは、http://world-silliest-bank-after.mybluemix.net/ で使用することができます。このセキュリティー・ポリシーはグローバルなものであることに注意してください。ポリシーが不規則に変更されるように見えるとしたら、それは誰かが同時にポリシーを変更しているからです。現在のセキュリティー・ポリシーを policy.html ファイルでダウンロードするためのボタンを使用すれば、その可能性を確かめることができます。

まとめ

この記事では、アクセス許可エンジンという重要なトピックにフォーカスするために、極めて単純なアプリケーションを使用しました。したがって、アプリケーションで使用しているセキュリティー・ポリシーも極めて単純なものとなっています。このような単純なアプリケーションとセキュリティー・ポリシーには Nools のようなルール・エンジンを使うまでのことはありませんが、実際のアプリケーションはこれよりも遥かに複雑です。ビジネス・ルールという手法を用いれば、アクセス許可エンジンがサード・パーティー・サーバーから追加の情報を (ファクトとして) 収集し、値がしきい値より大きいかどうか、あるいはユーザーが特定のグループのメンバーであるかどうかなどを確認することができます。

実際のアプリケーションに取り掛かるには、アクセス許可に関する以下の必要事項を検討してください。

  • ユーザー、つまり主語 (subject) は誰か?ユーザーに関するどのような情報が、アクセス許可の決定に関連する可能性があるのか?
  • 許可する必要があるアクション、つまり動詞 (verb) は何か?
  • これらのアクションのそれぞれが作用する目的語 (object) のタイプは何か?これらの目的語 (object) のどの属性が、アクセス許可の決定に関連する可能性があるのか?

決定を行うために必要な情報、そしてその情報を取得する方法を見極めた後、次のステップとなるのは、ユーザー・インターフェースを考案することです。(この記事でのように) 変数の値が等しいことをチェックすればよいだけなのか、それとも変数がグループのメンバーであるかどうか、あるいはしきい値を上回っているのか下回っているのかをチェックする必要があるのかを検討してください。また、技術系ではないユーザーでも理解して、プログラマーの助けがなくてもアクセス許可ポリシーを変更できるような各種オプションを提供する方法を考え出してください。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=セキュリティ, Cloud computing
ArticleID=1014036
ArticleTitle=ビジネス・ルールをアクセス許可エンジンとして使用する
publish-date=09032015