Dynamics CMR 4.0では、フィールドレベルのセキュリティは提供されません。今回は、[Dynamics CRM] フィールドレベルセキュリティ の一実装 Executeメッセージ編 に続いて、関連ビューや複数のエンティティレコードを検索するときに使用される RetrieveMultipe を対象にして、フィールドレベルのセキュリティを実装するサンプルを紹介します。

フィールドレベルセキュリティーにつていは、 下記リンクの Dynamics CRM 4.0 における、フィールドレベルセキュリティのホワイトペーパーを参考にしています。

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 Update Rollup 8
  • .NET 3.5, Visual Studio 2008

.NET3.0 以上で使用されるusing を除去すれば、.NET 2.0 でも動作すると思います。

1. 実装方針

1.1 プラグインによる実装

フィールドレベルのセキュリティはプラグイン(サーバ側)で実装します。javascript などクライアント側で項目を非表示、マスクなどをしても、知識のあるユーザは情報を取得する方法ができるためです。もちろん、マスクされたデータなどのフィールドをフォームから非表示にしてユーザビリティを向上させることは有用だと思います。

1.2 実装ステージ

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

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

2サンプル実装

Pre Stage でメールアドレスが 検索項目に含まれる場合、その検索項目から除外し、Post Stage において、売上高がマスク条件に該当する場合は、値をマスクしています。

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

namespace FieldlevelSecurityPlugin
{
    /// <summary>
    /// 検索 (RetrieveMultipleメッセージ)に対するフィールドレベルの
    /// セキュリティ設定の実装例
    /// 
    /// </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(MessageName.RetrieveMultiple))
                {
                    if (context.Stage == 10)
                    {
                        RemoveFieldInRetrieveMultiplePreStage(context);
                    }
                    else if (context.Stage == 50)
                    {
                        RemoveFiledInRetrieveMultiplePostStage(context);
                    }
                }
            }
        }

        #endregion

        #region RetrieveMultipleメッセージ
        /// <summary>
        /// 抽出列にアドレスがあった場合に削除
        /// フィールドレベルのセキュリティ用件によっては、LinkEntityを使用する場合を含むフィルタ条件
        /// やソート条件にセキュリティ対象のフィールドが含まれた場合例外をスローするなどの対策が必要
        /// </summary>
        /// <param name="context"></param>
        public void RemoveFieldInRetrieveMultiplePreStage(IPluginExecutionContext context)
        {
            if (context.PrimaryEntityName == Microsoft.Crm.SdkTypeProxy.EntityName.account.ToString())
            {
                // QueryExpression や QueryByAttribute かで細かく制御することも必要
                QueryBase query = context.InputParameters.Properties[ParameterName.Query] as QueryBase;
                ColumnSet columns = query.ColumnSet as ColumnSet;

                while (columns.Attributes.Contains("emailaddress1"))
                {
                    columns.RemoveColumn("emailaddress1");
                }
            }
        }
        /// <summary>
        /// 売上高が50000以上の取引先企業のフィールドをマスク.
        /// この処理だけでは不十分で、検索条件(ソート条件、フィルタ条件、LinkEntityを使用した関連のフィルタ条件)
        /// で、売上高などが使用された場合、Pre Stage でエラー(InvalidPluginExecutionExceptionをスロー)するなどの
        /// 対策が必要になる。
        /// </summary>
        /// <param name="context"></param>
        public void RemoveFiledInRetrieveMultiplePostStage(IPluginExecutionContext context)
        {
            if (context.PrimaryEntityName == Microsoft.Crm.SdkTypeProxy.EntityName.account.ToString())
            {
                BusinessEntityCollection entities = context.OutputParameters.Properties[ParameterName.BusinessEntityCollection]
                    as BusinessEntityCollection;

                foreach (DynamicEntity entity in entities.BusinessEntities)
                {
                    if (entity.Properties.Contains("revenue"))
                    {
                        CrmMoney m = entity.Properties["revenue"] as CrmMoney;
                        if (m != CrmMoney.Null && m.Value > 50000)
                        {
                            // 項目をブランクにする場合NULL値セット
                            // entity.Properties["revenue"] = CrmMoney.Null;

                            // 項目をマスク
                            m.Value = 999999999999;
                        }
                    }
                }
            }
        }
        #endregion

    }
}

実行結果は次のようになります。電子メールがブランクに、売上高がが50000より多い場合は999999999999にマスクされます。

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

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

サンプルでは実装されていませんが、抽出条件や、ソート条件にフィールドレベルセキュリティを適用する項目が含まれる場合は、例外(InvalidPluginExecutionException)をスローするなどをして、権限の名ユーザに情報が漏洩することを防ぐ必要があります。ソートや検索条件にフィールドレベルセキュリティを適用するフィールドを指定することで値を推測できてしまうためです。

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