SharePoint プロジェクト用の Azure カスタム クレーム プロバイダー パート 2

原文の記事の投稿日: 2012 年 2 月 15 日 (水曜日)

このシリーズのパート 1 では、高レベルで Windows Azure テーブル記憶領域を SharePoint カスタム クレーム プロバイダーのデータ ストアとして使用するという、このプロジェクトの目標の概要を示します。クレーム プロバイダーは CASI キット (英語)を使用して、ユーザー選択 (たとえば、アドレス帳) および入力コントロールの名前解決機能を提供するために必要なデータを Windows Azure から取得します。

パート 3 では、SharePoint ファーム内で使用するすべてのコンポーネントを作成します。たとえば、SharePoint と Azure の間の通信をすべて管理するカスタム コンポーネントを CASI キットに基づいて作成します。新規ユーザーに関する情報をキャプチャして Azure キューにプッシュするカスタム Web パーツも作成します。また、WCF を介して Azure テーブル記憶領域と通信し、入力コントロールおよびユーザー選択機能を有効にするカスタム クレーム プロバイダーも作成します。このカスタム クレーム プロバイダーも CASI キットに基づくカスタム コンポーネントです。

それでは、このシナリオを詳しく見ていきましょう。

この種のソリューションは、管理を最小限にしたエクストラネットが望まれるきわめて一般的なシナリオに最適です。たとえば、パートナーや顧客が Web サイトを検出してアカウントを要求できるようにし、さらに、要求されたアカウントを自動的に "プロビジョン" できるようにする必要がある状況が該当します。"プロビジョン" の意味の捉え方は人によって異なるでしょう。ここでは、基本的なシナリオとして使用していますが、もちろん、一部の処理をパブリック クラウド リソースに任せることもできます。

それでは、まず、私たちが独自に開発したクラウド コンポーネントについて説明します。

  • サポートするクレームの種類をすべて管理するテーブル。
  • ユーザー選択用の一意のクレーム値をすべて管理するテーブル。
  • 一意のクレーム値のリストに追加する必要があるデータの送信先キュー。
  • Azure テーブルからのデータの読み書き、およびキューへのデータの書き込みに使用するデータ アクセス クラス。
  • キューからデータを読み取って、一意のクレーム値テーブルに格納する Azure worker ロール。
  • SharePoint ファームが通信時にエンドポイントとして経由する WCF アプリケーション。SharePoint ファームは、この WCF アプリケーションを介して、クレームの種類のリストを取得、クレームを検索、クレームを解決、およびデータをキューに追加します。

次は、各クラウド コンポーネントについてもう少し詳しく説明します。

クレームの種類テーブル

クレームの種類テーブルには、カスタム クレーム プロバイダーが使用できるクレームの種類がすべて格納されます。このシナリオでは、クレームの種類として 1 つだけ ID クレームを使用します。このケースでは、電子メール アドレスを ID クレームとして使用します。別のクレームを使用してもかまいませんが、ここではわかりやすいシナリオにするために電子メール アドレスを使用しています。Azure テーブル記憶領域では、テーブルにクラスのインスタンスを追加します。したがって、クレームの種類を記述するクラスを作成する必要があります。もちろん、Azure の同じテーブルに、別のクラスの種類のインスタンスを追加することもできますが、ここではシナリオをわかりやすくするために、そうした処理は行いません。このテーブルが使用するクラスは次のとおりです。

namespace AzureClaimsData

{

    public class ClaimType : TableServiceEntity

    {

 

        public string ClaimTypeName { get; set; }

        public string FriendlyName { get; set; }

 

        public ClaimType() { }

 

        public ClaimType(string ClaimTypeName, string FriendlyName)

        {

            this.PartitionKey = System.Web.HttpUtility.UrlEncode(ClaimTypeName);

            this.RowKey = FriendlyName;

 

            this.ClaimTypeName = ClaimTypeName;

            this.FriendlyName = FriendlyName;

        }

    }

}

 

ここでは、Azure テーブル記憶領域の基本事項については説明しません。これらの基本事項については、既に多数の資料で説明されています。PartitionKey または RowKey とは何か、またはその使用方法について詳しく知りたい場合は、お手元の Bing 検索エンジンで調べてください。ここで 1 つだけ説明する必要があるのは、PartitionKey 用に格納する値を Url エンコードするという点です。こうした処理を行うのはなぜでしょうか? それは、このケースでは、PartitionKey はクレームの種類です。PartitionKey は、urn:foo:blah、https://www.foo.com/blah など、いくつかの形式を取ることがあります。たとえば、クレームの種類にスラッシュが含まれていることがあります。しかし、Azure では PartitionKey にスラッシュが含まれていると格納できません。そのため、スラッシュは Azure フレンドリな形式にエンコードされます。前述のとおり、ここでは電子メール クレームを使用するため、そのクレームの種類は https://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress です。

一意のクレーム値テーブル

一意のクレーム値テーブルには、取得した一意のクレーム値がすべて格納されます。このケースでは、クレームの種類として 1 つだけ ID クレームを格納します。定義上、クレーム値はすべて一意です。ただし、このアプローチを採用した理由は拡張性にあります。たとえば、将来、この状況でロール (Role) クレームを使用する必要が生じたとします。ロール (Role) クレーム "従業員 (Employee)" または "顧客 (Customer)" などを個別に 1,000 回格納しても意味がありません。ユーザー選択の場合、値が存在することがわかるだけで、その値はユーザー選択で使用できます。その後、その値はだれかが必ず持っています。つまり、サイト内で権限を付与するときにその値を使用できるようにするだけで十分ということです。以上のことを踏まえて、一意のクレーム値を格納するクラスは次のようになります。

namespace AzureClaimsData

{

    public class UniqueClaimValue : TableServiceEntity

    {

 

        public string ClaimType { get; set; }

        public string ClaimValue { get; set; }

        public string DisplayName { get; set; }

 

        public UniqueClaimValue() { }

 

        public UniqueClaimValue(string ClaimType, string ClaimValue, string DisplayName)

        {

            this.PartitionKey = System.Web.HttpUtility.UrlEncode(ClaimType);

            this.RowKey = ClaimValue;

 

            this.ClaimType = ClaimType;

            this.ClaimValue = ClaimValue;

            this.DisplayName = DisplayName;

        }

    }

}

 

ここで、いくつかの点について説明する必要があります。まず、前のクラスと同様、PartitionKey はクレームの種類で、スラッシュが含まれているため、UrlEncoded 値を使用します。また、Azure テーブル記憶領域を使用するときに頻繁に見られますが、SQL のような JOIN 概念がないため、データは非正規化されます。技術的には、JOIN は LINQ 内で行えますが、より簡単に非正規化できると私自身が判断する Azure データの操作時には LINQ 内の非常に多くの要素が無効になっています (無効になっていないと、実行に悪影響が及びます)。このことに関して別の考えがあれば、コメントを投稿してください。みなさんがどのようにお考えであるか興味があります。このケースでは、このクラスに格納するクレームの種類は Email なので、表示名は "Email" です。

クレーム キュー

クレーム キューは非常に単純です。ここでは、このキューに "新規ユーザー" の要求を格納します。また、Azure ワーカー プロセスは、このキューからデータを読み取って、そのデータを一意のクレーム値テーブルに移します。この処理を行う主な理由は、Azure テーブル記憶領域は非常に潜在的になることがありますが、キュー内のアイテムを固定すると、かなり高速になるからです。このアプローチを採用すると、SharePoint Web サイトへの影響を最小限に抑えることができます。

データ アクセス クラス

Azure テーブル記憶領域およびキューの操作に関するいくらか日常的な側面として、独自のデータ アクセス クラスを常に記述する必要があります。テーブル記憶領域の場合、データ コンテキスト クラスとデータ ソース クラスを記述する必要があります。これについて説明する時間は取りません。詳しい情報は Web に記載されています。また、Azure プロジェクトのソース コードをこの投稿に添付しているので、そちらもご覧ください。

ここでは、重要事項について説明します。重要事項と言っても、個人用スタイルの選択についてです。私はすべての Azure データ アクセス コードを個々のプロジェクトに分割するのを好みます。こうすると、個々のプロジェクトを独自のアセンブリにコンパイルできます。また、個々のプロジェクトを Azure プロジェクト以外のプロジェクトから使用できるようにもなります。たとえば、私がアップロードするサンプル コードには、Azure バック エンドのさまざまなパーツ テストで使用した Windows フォーム アプリケーションがあります。この Windows フォーム アプリケーションには Azure 用の設定が何も行われていませんが、いくつかの Azure アセンブリと私自身のデータ アクセス アセンブリへの参照が構成されています。この Windows フォーム アプリケーションは、そのプロジェクトで使用できます。また、SharePoint 用のデータ アクセスのフロントエンドに使用する私の WCF プロジェクトでも同様に簡単に使用できます。

次に、データ アクセス クラスの詳細についていくつか示します。

  •          取得するデータ (クレームの種類および一意のクレーム値) のために別個の "コンテナー" を用意しています。コンテナー クラスを用意するということは、List<> 型のパブリック プロパティを持つ単純なクラスを用意するということです。私はデータが要求されたら、結果の List<> ではなく、このクラスを返します。なぜそのようにするかと言うと、Azure から List<> を返すと、クライアントはリスト内の最後の項目のみを取得します (同じ処理をローカルでホストされる WCF から実行すると正常に機能します)。この問題を回避するために、私はクレームの種類を次のようなクラスに返します。

namespace AzureClaimsData

{

    public class ClaimTypeCollection

    {

        public List<ClaimType> ClaimTypes { get; set; }

 

        public ClaimTypeCollection()

        {

            ClaimTypes = new List<ClaimType>();

        }

 

    }

}

 

一意のクレーム値は、次のようなクラスを返します。

namespace AzureClaimsData

{

    public class UniqueClaimValueCollection

    {

        public List<UniqueClaimValue> UniqueClaimValues { get; set; }

 

        public UniqueClaimValueCollection()

        {

            UniqueClaimValues = new List<UniqueClaimValue>();

        }

    }

}

 

 

  •          データ コンテキスト クラスは非常に単純です。友人 Vesa の言葉を借りると、何の魅力もないものです。次のようなクラスです。

 

namespace AzureClaimsData

{

    public class ClaimTypeDataContext : TableServiceContext

    {

        public static string CLAIM_TYPES_TABLE = "ClaimTypes";

 

        public ClaimTypeDataContext(string baseAddress, StorageCredentials credentials)

            : base(baseAddress, credentials)

        { }

 

 

        public IQueryable<ClaimType> ClaimTypes

        {

            get

            {

                //this is where you configure the name of the table in Azure Table Storage

                //that you are going to be working with

                return this.CreateQuery<ClaimType>(CLAIM_TYPES_TABLE);

            }

        }

 

    }

}

 

  •          データ ソース クラスでは、少し異なるアプローチで Azure に接続します。Web で見られるほとんどの例では、いくつかの reg 設定クラスを利用して資格情報を読み取ろうとします (このクラス名は正確ではありません。どのような名前であったか記憶していません)。このアプローチには問題があります。それは、私はデータ クラスを Azure 外で使用するつもりなので、Azure 固有のコンテキストがないということです。したがって、プロジェクト プロパティに Setting を作成し、そこに、Azure アカウントへの接続に必要なアカウント名およびキーを取り込みます。両方のデータ ソース クラスに、Azure 記憶領域への接続を作成する次のようなコードがあります。

 

        private static CloudStorageAccount storageAccount;

        private ClaimTypeDataContext context;

 

 

        //static constructor so it only fires once

        static ClaimTypesDataSource()

        {

            try

            {

                //get storage account connection info

                string storeCon = Properties.Settings.Default.StorageAccount;

 

                //extract account info

                string[] conProps = storeCon.Split(";".ToCharArray());

 

                string accountName = conProps[1].Substring(conProps[1].IndexOf("=") + 1);

                string accountKey = conProps[2].Substring(conProps[2].IndexOf("=") + 1);

 

                storageAccount = new CloudStorageAccount(new StorageCredentialsAccountAndKey(accountName, accountKey), true);

            }

            catch (Exception ex)

            {

                Trace.WriteLine("Error initializing ClaimTypesDataSource class: " + ex.Message);

                throw;

            }

        }

 

 

        //new constructor

        public ClaimTypesDataSource()

        {

            try

            {

                this.context = new ClaimTypeDataContext(storageAccount.TableEndpoint.AbsoluteUri, storageAccount.Credentials);

                this.context.RetryPolicy = RetryPolicies.Retry(3, TimeSpan.FromSeconds(3));

            }

            catch (Exception ex)

            {

                Trace.WriteLine("Error constructing ClaimTypesDataSource class: " + ex.Message);

                throw;

            }

        }

 

  •          データ ソース クラスの実際の実装には、クレームの種類と一意のクレーム値の両方を表す新しいアイテムを追加するメソッドがあります。次に示すように、このメソッドは非常に簡単なコードです。

 

        //add a new item

        public bool AddClaimType(ClaimType newItem)

        {

            bool ret = true;

 

            try

            {

                this.context.AddObject(ClaimTypeDataContext.CLAIM_TYPES_TABLE, newItem);

                this.context.SaveChanges();

            }

            catch (Exception ex)

            {

                Trace.WriteLine("Error adding new claim type: " + ex.Message);

                ret = false;

            }

 

            return ret;

        }

 

一意のクレーム値のデータ ソース用の Add メソッドには 1 つだけ重要な違いがあります。それは、変更の保存時に例外が発生してもエラーをスローしない、または false を返さないという点です。なぜそのようになっているかと言うと、人が誤っている場合もあれば、人によっては何回か試行した末にサインアップする場合もあるということを十分に想定しているからです。ただし、このケースでは、電子メール クレームのレコードをいったん取得すると、その後の追加試行では例外がスローされます。Azure は、厳密に型指定された例外という恵まれた機能を備えていないうえ、私自身もトレース ログが要領を得ない情報であふれるのを望まないので、そうした状況が発生しても気にしないようにしています。

  •          クレームの検索についてはいくらか興味深い話があります。興味深いと言っても、LINQ では実行できる操作が Azure を伴う LINQ では実行できなくなるいうことを再度暴露するという意味ですが。ここでは、まずコードを追加します。その後、いくつかの選択肢について説明します。

 

        public UniqueClaimValueCollection SearchClaimValues(string ClaimType, string Criteria, int MaxResults)

        {

            UniqueClaimValueCollection results = new UniqueClaimValueCollection();

            UniqueClaimValueCollection returnResults = new UniqueClaimValueCollection();

 

            const int CACHE_TTL = 10;

 

            try

            {

                //look for the current set of claim values in cache

                if (HttpRuntime.Cache[ClaimType] != null)

                    results = (UniqueClaimValueCollection)HttpRuntime.Cache[ClaimType];

                else

                {

                    //not in cache so query Azure

 

                    //Azure doesn't support starts with, so pull all the data for the claim type

                    var values = from UniqueClaimValue cv in this.context.UniqueClaimValues

                                  where cv.PartitionKey == System.Web.HttpUtility.UrlEncode(ClaimType)

                                  select cv;

 

                    //you have to assign it first to actually execute the query and return the results

                    results.UniqueClaimValues = values.ToList();

 

                    //store it in cache

                    HttpRuntime.Cache.Add(ClaimType, results, null,

                        DateTime.Now.AddHours(CACHE_TTL), TimeSpan.Zero,

                        System.Web.Caching.CacheItemPriority.Normal,

                        null);

                }

 

                //now query based on criteria, for the max results

                returnResults.UniqueClaimValues = (from UniqueClaimValue cv in results.UniqueClaimValues

                           where cv.ClaimValue.StartsWith(Criteria)

                           select cv).Take(MaxResults).ToList();

            }

            catch (Exception ex)

            {

                Trace.WriteLine("Error searching claim values: " + ex.Message);

            }

 

            return returnResults;

        }

 

最初に注意する点があります。それは、Azure データには StartsWith を使用できないということです。つまり、データをすべてローカルに抽出してから StartsWith 式を使用する必要があります。すべてのデータを抽出する処理は負担が大きくなるため、すべてのデータを抽出したらデータをキャッシュしておきます (すべての行を抽出する場合は、テーブル スキャンが効果的です)。この方法では 10 分おきに "実際の" 再呼び出しを行うだけです。ただし、欠点もあります。それは、その間にユーザーが追加された場合、キャッシュの有効期限が切れて、すべてのデータを再度抽出するまでは、それらのユーザーをユーザー選択に表示できないということです。結果を見ているのがいつであるかを覚えておいてください。

実際にデータ セットがあれば、StartsWith を実行できます。また、取得するレコード数を制限することもできます。既定では、SharePoint のユーザー選択には 200 個以下のレコードしか表示されません。したがって、このメソッドの呼び出し時に要求する最大数は 200 です。ただし、ここでは、これをパラメーターとして使用するため、自分が望むように処理を実行できます。

キュー アクセス クラス

正直に言うと、これに関しては特に興味深い話はありません。メッセージをキューに追加、読み取る、および削除する基本的なメソッドです。

Azure worker ロール

worker ロールについても特に説明することはありません。10 秒おきに起動して、新規メッセージがないかどうかキューを確認します。この確認は、キュー アクセス クラスを呼び出して行われます。キューに何らかのアイテムが見つかると、その内容 (セミコロンで区切られている) を構成要素パーツに分割し、UniqueClaimValue クラスの新規インスタンスを作成して、そのインスタンスを一意のクレーム値テーブルに追加しようとします。このインスタンスがテーブルに追加されると、キューからメッセージが削除され、次のアイテムに移動します。この処理は、一度に読み取り可能なメッセージの最大数 (32) に達するか、残りのメッセージがなくなるまで行われます。

WCF アプリケーション

前述のとおり、WCF アプリケーションは SharePoint コードの通信先として、キューにアイテムを追加するとき、クレームの種類のリストを取得するとき、クレーム値を検索または解決するときに使用されます。信頼できるアプリケーションと同様に、WCF アプリケーションは、呼び出し元の SharePoint ファームとの間に信頼関係を構築します。これにより、データの要求時にトークン スプーフィングなどの攻撃を防止します。現時点では、WCF 自体に、より強力なセキュリティは実装されていません。完全性を求めるために、WCF は最初にローカル Web サーバーでテストを行ってから Azure に移され、さらに、Azure で再度テストを行って、すべてが正常に機能することが確認されています。

このソリューションの Azure コンポーネントの基本事項についての説明は以上です。これらの説明を通じて、個々の移動パーツについて、また、移動パーツの使用方法について理解していただければ幸いです。次のパートでは、SharePoint カスタム クレーム プロバイダーについて、および "ターンキー" エクストラネット ソリューションのためにこれらのパーツをすべてフックする方法について説明します。この投稿に添付されるファイルには、データ アクセス クラス、テスト プロジェクト、Azure プロジェクト、worker ロール、および WCF プロジェクトのすべてのソース コードが含まれています。また、この投稿のコピーが Word 文書で含まれています。このコンテンツについて私が説明している内容を実際に理解したうえで、このサイトで変換してください。そうでないと、すべて台無しになります。

これはローカライズされたブログ投稿です。原文の記事は、The Azure Custom Claim Provider for SharePoint Project Part 2 をご覧ください。