Dynamics CMR 4.0では、フィールドレベルのセキュリティは提供されません。今回は、 下記リンクの Dynamics CRM 4.0 における、フィールドレベルセキュリティのホワイトペーパーを参考に 検索処理 (Execute メッセージ) に対するフィールドレベルセキュリティをサンプル実装しみました。

Security and Authentication in Microsoft Dynamics CRM: Field-level Security in Microsoft Dynamics CRM: Options and Constraints
http://www.microsoft.com/downloads/details.aspx?displaylang=en&FamilyID=471f8670-47b3-4525-b25d-c11a6774615c

対象は取引先企業エンティティとし、次の機能を提供します。

  • メールアドレスを検索対象から除外
  • 売上高50000以上の企業の売上高をマスク

動作確認環境

  • Dynamics CRM 4.0 Rollup 8
  • .NET 3.5, Visual Studio 2008

.NET 3.5 を使用していますが、LINQを使用している箇所を書き換えれば .NET 2.0 でも動作します。

1. 実装方針

1.1 プラグインによる実装

高度な検索画面からなど取引先企業を検索できますが、javascript から制御する方法がないこと、また、クライアントにデータが到着した時点でマスクしても、知識のあるユーザは情報を取得する方法があることから、フィールドレベルのセキュリティはプラグインから実装します。

1.2 実装ステージ

メールアドレスを検索対象から除外するために、 Execute メッセージの Pre Stage で、検索条件からメールアドレスを除外します。ExecuteメッセージのPre Stage で検索対象から除外することで検索処理のパフォーマンスを完全で着ます。

売上高50000以上の売上高をマスクするために、検索結果 Execute メッセージの Post Stage で処理を実装します。これは検索結果を確認しないとセキュリティを適用できないため。つまり、ロジック処理が必要なためです。

2サンプル実装

Pre Stage と Post Stage でそれぞれ FetchXml, FetchXmlResult から得られる XML を編集しています。わかりにくいかもしれませんが、関連として指定された場合も要件を満たせるように実装しています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Crm.Sdk;
using System.Xml;
using System.Xml.Linq;

namespace FieldlevelSecurityPlugin
{
    /// <summary>
    /// 検索 (Executeメッセージ)に対するフィールドレベルの
    /// セキュリティ設定の実装例
    /// 
    /// Preステージで、取引先企業のメールアドレスを検索列から除外
    /// Postステージで、検索された取引先企業のうち売上高が50000以上の売上高をマスクする
    /// </summary>
    public class FieldLevelPlugin : IPlugin
    {

        #region IPlugin メンバ
        /// <summary>
        /// プラグインのエントリ
        /// </summary>
        /// <param name="context"></param>
        public void Execute(IPluginExecutionContext context)
        {
            if (context.CallerOrigin is ApplicationOrigin)
            {

                if (context.MessageName.Equals("Execute"))
                {
                    if (context.Stage == 10)
                    {
                        // pre
                        RemoveFieldInPreStage(context);
                    }
                    else if (context.Stage == 50)
                    {
                        // post
                        RemoveFieldInPostStage(context);
                    }
                }
            }
        }

        #endregion

        /// <summary>
        /// 特定の列を表示する処理を禁止する例
        /// 取引先企業のメールアドレスを表示列から除外
        /// </summary>
        /// <param name="context"></param>
        public void RemoveFieldInPreStage(IPluginExecutionContext context)
        {
            string FetchXml = "FetchXml";
            if (context.InputParameters.Contains(FetchXml))
            {
                string fetchXml = context.InputParameters[FetchXml] as string;
                XElement xe = XElement.Parse(fetchXml);
                // 特定の属性を検索対象から除外
                List<XElement> elements = (from x in xe.Descendants("attribute")
                                           where (x.Attribute("name").Value == "emailaddress1")
                                                && (x.Parent.Attribute("name").Value == "account")
                                            select x).ToList();

                // 特定の属性をソート対象から除外
                elements.AddRange((from x in xe.Descendants("order")
                              where x.Attribute("attribute").Value == "emailaddress1"
                              && x.Parent.Attribute("name").Value == "account"
                              select x));

                foreach (XElement e in elements)
                {
                    e.Remove();
                }
                if (elements.Count > 0) context.InputParameters[FetchXml] = xe.ToString(SaveOptions.DisableFormatting);
            }
        }
        /// <summary>
        /// 検索結果にロジックを適用指定表示、非表示を決定する例
        /// 取引先企業の中で、売上高が50000以上の値を------に設定する。
        /// </summary>
        /// <param name="context"></param>
        public void RemoveFieldInPostStage(IPluginExecutionContext context)
        {
            string FetchXmlResult = "FetchXmlResult";
            string FetchXml = "FetchXml";

            if (context.InputParameters.Contains(FetchXml))
            {
                if (!context.OutputParameters.Contains(FetchXmlResult)) return;

                string fetchXml = context.InputParameters[FetchXml] as string;
                string fetchXmlResult = context.OutputParameters[FetchXmlResult] as string;
                
                XElement xe = XElement.Parse(fetchXml);

                List<string> prefix = new List<string>();
                if (xe.Element("entity").Attribute("name").Value == "account")
                {
                    prefix.Add(string.Empty);
                }
                var query = from e in xe.Descendants("link-entity")
                            where e.Attribute("name").Value == "account"
                            select e.Attribute("alias");
                foreach (XAttribute a in query)
                {
                    prefix.Add(a.Value + ".");
                }

                XElement xresult = XElement.Parse(fetchXmlResult);
                foreach (XElement e in xresult.Elements("result"))
                {
                    foreach (string pfx in prefix)
                    {
                        XElement e1 = e.Element(pfx + "revenue");
                        if (e1 != null)
                        {
                            decimal d;
                            if (decimal.TryParse(e1.Value, out d))
                            {
                                if (d >= 50000m)
                                {
                                    e1.Value = "------";
                                    e1.Attribute("formattedvalue").SetValue("-----");
                                }
                            }
                        }
                    }
                }
                context.OutputParameters[FetchXmlResult] = xresult.ToString(SaveOptions.DisableFormatting);
            }
        }
    }
}

プラグインを登録して、動かした結果は下図のようになります。売上高が-----でマスクされ、電子メールはすべてブランクになります。関連する(LOOKUP先)親取引先企業の電子メール、売上高もフィールドレベルセキュリティが適用されていることが確認できます。

3. Execute メッセージにたいするField Level Securityのまとめ

サンプル実装では、特定の列を無条件に検索対象から除外する処理と、検索結果に対して特定のセキュリティを適用する処理を実装しました。

サンプルでは、ソート条件にメールアドレスが指定されている場合もソート条件から要素(order)を除外しています。これは、order 要素が存在すると、そのフィールドが検索結果に現れるためです。また、フィールドの属性が金額などの場合はセキュリティを適用すべきユーザに不要な情報(昇順、降順)を与えてしまうことを防ぐことができます。

さらに、サンプルでは実装されていませんが、検索条件にセキュリティを適用するフィールドが含まれていた場合も InvalidPluginExecutionException をスローするなどして対処する必要があります。この対応をしないと、検索条件からマスクしている金額情報などの情報を特定することができてしまいます。

xml を解析し、フィールドレベルのセキュリティをもれなく実装するのは骨の折れる作業です。個別に対応するのではなく、セキュリティを適用するユーザーが対象のフィールド(属性)を検索対象、ソート条件、抽出条件などに指定した場合は、無条件で InvalidPluginExecutionException をスローしてセキュリティを実装するのも一つの手だと思います。

説明は以上です。誤り、指摘点などありましたらご連絡ください。