はじめに
Windows SharePoint Services 3.0およびMicrosoft Office SharePoint Server 2007は管理者がサイトやコンテンツへのアクセスをコントロールできる優れたセキュリティモデルを備えています。ユーザおよびグループに特定のオブジェクト(サイト、リスト、ライブラリ、フォルダ、個人のドキュメントなど)へのアクセス権限を与えることができるのです。
しかしリストやドキュメントライブラリ内の特定の列についてアクセス権限を設定したいことも少なからずあります。現在のSharePointには列やビューを保護する機能はありません。通常このようなことが必要なのはクライアントや従業員に関する多くの情報を格納するリストです。理想的なのは、特定の列(給与、総所得、販売促進対象者か、などの列)をポータル内の特定のグループにだけ見せられるようにすることでしょう。
このような要望を解決するために、SharePointの拡張性と標準アイテムを使って、独自のフィールド型について列レベルの権限を適用できるようにする方法をここで説明していきます。今回の方法ではカスタマイズしたルックアップフィールドを列として使います。それによりセキュアに値を保持し、アクセス権限を持ったユーザによる参照にだけ保護された値を返すメソッドを持ったリストが作れるようになります。
そうすることで承認されたユーザは保護された列の内容を普通の列と同じ様に照会モードで見ることができ、そうでないユーザにはその列の内容が全く見えなくなります。図1はその実例です。同じように、新規作成モードや編集モードでも承認されたユーザだけが保護列の内容にアクセスすることができます。
図1 承認されたユーザとそうでないユーザでの保護列の違い
列レベルセキュリティのアーキテクチャ
独自の列レベルセキュリティを設計するには次にあげる問題点を解決する必要があります。
- 保護されたデータが認証されないユーザに読み取られないようにセキュアな方法でデータを格納する
- 認証されたユーザだけに見えるようデータを描画する
- 認証されたユーザだけが保護されたデータを変更できるようにする
これらの問題を解決するために私たちはCustom Field Typeをデータの描画や処理に、そしてItem Level Securityをデータ格納のために使うことにしました。
Content TypeとList Formと並んでCustom Field TypeはWindows SharePoint Servicesの拡張性を担う基本的な機構で、データのアクセスや描画や処理をカスタマイズできるようにします。Custom Filed Typeの詳細についてはここ(リンク)にあるWindows SharePoint Services 3.0 SDKで参照できます。
今回作ったCustome Field Typeの一番の役割は、適切な認証を受けたユーザだけがデータの照会や変更をできるようにすることです。今回はSharePointのどのリストでも使うことのできるSecure ColumnというCustom Filed Typeを実装することにしました。
データの格納については、別のデータベースにあるテーブルが使えるようにすることを検討しました。これは既にSharePointと別のデータベースを使っている時には良いことです。しかし多くのSharePointシステムがSharePointでないデータベースを使ってはいないことを考え、また今回の記事の目的からして、SharePoint Listを格納機構として使うことに決めて、個々のデータベースに依存しないようにしました。
SharePoint Listを保護データの格納に使うことで、各データを保護するためのList Itemを作ることができるようになります。そしてセキュリティ保障を十分なレベルにするためにItem-Level Securityの機能を使うことができます。これは私たちの列レベルセキュリティがSharePointの備えるItem Level Securityと同じだけセキュアなものになることを意味します。
ただしSharePoint Listとルックアップ列の機能を用いることで制約も生じます。その制約とは、巨大なリストに対する参照はパフォーマンスを妨げる可能性があり、またルックアップ列は別のSite Collectionにあるリストには使えないということです。
実装の詳細
列レベルセキュリティを実装するにはいくつかのコンポーネントを独自に用意する必要があります。
- Data Storage List
- Custom Field Type(カスタムフィールドとそれ用のFieldEditorコントロールを含む)
- Deployment Solution Package
これらのコンポーネントは図2に表されているように連携し、それによってユーザは保護列に格納されるデータを作ったり見たり編集したりできるようになります。
図2 実装の概要
今回の実装例では単一のData Storage ListをSite Collectionの全てのデータについて用いることにします。これによりメンテナンスが簡単になりますし、システムもシンプルになります。デメリットとなるのは全ての保護フィールドが1つのオブジェクトに負担させられることです。大規模システムではこのことでパフォーマンスの問題が起きる可能性があります。もしそのような問題が起きた場合は各保護フィールドごとにData Storage Listを作るようにすることができます。
Data Storage List
データをセキュアに格納するのにSharePointのリストを使うとItem-Level SecurityやAPIの活用といったさまざまなメリットがあります。しかし一方ではいくつかの課題も生じます。
- 可視性 - ノーマルのSharePoint Listは権限のあるユーザであれば誰でも見ることができます。
- スケーラビリティ - 全ての保護列に関するシステムの全データを単一のリストに格納しようとすると、リスト内のアイテム数が非常に多くなりSharePointの推奨する限度を超えてしまう可能性があります。そうなるとパフォーマンスが大幅に下がることになります。
- 権限のメンテナンス - Item-Level Securityを各保護アイテムに用いると、列の権限を更新するのが非常に難しくなります。
- 生成のタイミング - リストが存在しない時にユーザが新しい列を追加しようとすることがあります。
これらの課題はSharePointリストが持つ機能、あるいは今回作るCustome Filed Typeにコードを追加することで解決することができます。
可視性の問題については、対象のリストを「カタログ」リストとして作ることで対処できます。これはSharePoiontの標準システムリストを生成する時にも用いられる方法です。たとえばウェブパーツギャラリ、サイトテンプレートギャラリ、マスタページライブラリなどがそうです。さらにSPListオブジェクトの次のプロパティリストをセットすることで可視性を補強しています。
- Hidden - この値をtrueにセットすることで全ての標準UI要素からそのリストを除くことができます。
- NoCrawl - この値をtrueにセットすることでそのリストのデータをSharePointのクロールエンジンの対象にしないようにできます。
- OnQuickLaunch - この値をfalseにセットすることでこのリストがクイック・ランチ・ナビゲーションに表示されないようにできます。
スケーラビリティと権限メンテナンスの問題については、複数アイテムのグループを格納するのにフラットなリストは使わず、2階層のフォルダ構造を用いることにしました。このフォルダ構造の第1のレベルは与えられた保護列が属するリストに対応し、第2のレベルはその保護列自体に対応するものです。こうすることで、たとえリストに複数の保護列があったとしても、リストアイテム数の最も多い保護列のリスト以上にはアイテム数が増えることがありません。この方法でスケーラビリティの問題を解決すると、2レベルのフォルダ構造持つことになるので、第2レベルのフォルダの権限を所定の保護列の権限に関連づけることにします。これにより各アイテムごとにパーミッションをメンテナンスする必要もなくなります。
図3 フォルダ構造
最後にこのリストの生成のタイミングについてですが、これはCustom Field Typeの一部として扱うと簡単にできます。そのためには、新しいフィールドが作成された時にそのリストが存在しなかった場合、リストを明示的に生成するようコーディングする必要があります。
Custom Field Type
上記アーキテクチャのセクションで述べたように、データはSharePoint Listに作成されたSecure Column(今回のCustom Field Type)によって扱われ、リスト中には直接格納されません。そのかわりにData Storage Listという専用のリストへ格納します(このリストは_SecureFieldStorageの内部的な名前とサイトの相対URLは _catalogs/_SecureFieldStorageを持ちます)。このData Storage Listに格納されたデータを取得してホストリスト(たとえばSecure Columnを内包するリスト)のコンテキストで表示すること、これが私たちのCustom Field Typeの一番の目的です。
この機能の実装を考えた時、SPFieldクラスから今回のCustom Field Typeを作らないといけないようなことは避けたいと考えました。そしてそうする代わりにSharePointの既存機能をできるかぎり有効活用しようと考えました。SharePointのフィールド型に備わっていることを自分たちのCustom Field Typeのベースに利用しようとした時、ルックアップフィールド(SPFieldLookup)に白羽の矢が立ちました。
ルックアップフィールドの機能のうち私たちが一番利用したいと考えたのは、別のSharePointリストにあるフィールドを参照して、その中にあるアイテムで指定したIDを持つアイテムからフィールド値を取得する機能です。この機能は私たちのにも必要なものです。さらに、SQLのジョインを使っている内部実装や、セキュアでありながらスケーラビリティを維持できるといったルックアップフィールドの持つ利点を活かすことができます。
ルックアップフィールドは、そのコア機能が私たちの必要とするものの素晴らしい土台となる一方で、標準のルックアップフィールドと私たちに必要なものの間には多くのミスマッチもあります。それが理由で私たちはSPLookupFieldを拡張するコードを書くことにしました。その拡張とは次のことです。
- ユーザがルックアップ列の関係するリストを変更できないようにする
- ユーザがその列に関係するデータについての権限を指定できないようにする
- 新しい列が生成された時は、関係するリストや列をカスタムData Storage Listに自動的にセットするようにする
- そのカスタムData Storage Listが存在しない時は自動的に生成されるようにする
- このフィールドの値を表示する時には、関係するリストアイテムへのハイパーリンクとして描画されるようにする
- 編集あるいは新規作成モードでは、このフィールドは標準のテキスト列と同じように描画し、変更はすべて対象となっているリストに反映させるようにする
- 編集あるいは新規作成モードで、ユーザが権限を持ってない時は、このフィールドのデータの値が非表示になるようにする。
Custom Field Type Xml
Custom fldtypes.xmlファイルはどんなCustom Filed Typeの実装においても重要となるコンポーネントです。今回の場合、この中で私たちのCustom Field Type Class(後述)を指すように設定します。
<FieldName="FieldTypeClass"> SecureField.SecureField, SecureField, Version=1.0.0.0, Culture=neutral, PublicKeyToken=48a15d1316dd0f7d Field>
また今回はカスタム表示パターンも含めます。ルックアップ列はデフォルトでハイパーリンクとして描画されますが、表示パターンを次のように指定することでそうならないようにします。
<LookupColumn HTMLEncode ="TRUE" "AutoHyperLink="FALSE""/>
他に拡張したのは、カスタムフィールドのエディタコントロールを指定したことです。
<FieldName="FieldEditorUserControl">/_controltemplates/SecureFieldEditor.ascx Field>
Custom Field Type Class
ルックアップフィールドのコア機能をカスタマイズする上で一番手のかかるのがCustom Field Type Classです。その機能には次のことが含まれます。
- Data Storage Listを作成する
- Data Storage Listの構造を作成する
- Data Storage Listの権限を設定する
- 今回のカスタムフィールド用コントロールと関連づける
- フィールドが削除されたらData Storage Listを再設定する
今回のCustom Field Type ClassではSPFieldLookupを継承し、保護列が作成されたり編集されたりした時にそのベースになっているData Storage Listも適切な権限を持って作成されているようにUpdateメソッドをオーバーライドします。
public override void Update()
{
SPSecurity.RunWithElevatedPrivileges(EnsureSecureFieldStorageListExists);
SPWeb web = SPContext.Current.Site.RootWeb;
this.LookupWebId = web.ID;
this.LookupField = secureFieldStorageFieldName;
this.LookupList = web.Lists[secureFieldStorageListName].ID.ToString();
RetrieveCustomProperties();
SPSecurity.RunWithElevatedPrivileges(ApplyPermissions);
base.Update();
}
ここでの処理は全て管理者レベルの権限でおこなわれます。これは標準ユーザが新しい保護列を作成する際にでもエラーが起きないようにするためです。
さらに、このカスタマイズをおこなうにはFieldRenderingControlプロパティをオーバーライドしてカスタムフィールド用コントロールが使われるようにする必要があります。
public override BaseFieldControl FieldRenderingControl
{
get
{
BaseFieldControl control = new SecuredFieldControl();
control.FieldName = this.InternalName;
return control;
}
}
最後に、OnDeletingメソッドをオーバーライドして列が削除された時に関連データがすべて消去されるようにします。
public override void OnDeleting()
{
base.OnDeleting();
SPSecurity.RunWithElevatedPrivileges(removeFieldFolder);
}
Custom Field Editor Control
ユーザが保護列に対して権限を与えられるようにするには、コアとなるField Type Classに加え、Custom Field Editor Controlも必要となります。保護列の権限は、リストに保護列が追加された時だけでなくいつでも設定できるようにします。そのために SharePoint PeopleEditorコントロールを利用できる新しいユーザコントロールを使います。このコントロールによりユーザはプリンシパル(権限をもつユーザやグループ)を検索したり選択することができます。
<sharepoint:PeopleEditor ID="AllowedPrincipalsPeoplePicker" runat="server" AutoPostBack="false" PlaceButtonsUnderEntityEditor="true" SelectionSet="SPGroup" MultiSelect="true" />
セキュリティ設定をフィールドから設定したり取得したりするために、このユーザーコントロールのコードビハインドクラス(コントロールの処理を実装するクラス)でIFieldEditorインターフェースを実装します。とりわけ、InitializeWithFieldメソッドがそのフィールドのセキュリティ設定を取得するのに使われます。
if (Page.IsPostBack)
return;
// Initialize the people picker control using comma separated account list from the secure field
SecureField secureField = (SecureField)field;
if (secureField != null && secureField.AllowedPrincipals != null)
{
StringBuilder accounts = new StringBuilder();
foreach (object entity in secureField.AllowedPrincipals)
{
accounts.Append((entity as PickerEntity).Key);
accounts.Append(',');
}
this.AllowedPrincipalsPeoplePicker.CommaSeparatedAccounts = accounts.ToString();
this.AllowedPrincipalsPeoplePicker.Validate();
}
そして、OnSaveChangeメソッドがフィールドのセキュリティ設定を更新するのに用いられます。
AllowedPrincipalsPeoplePicker.Validate();
SecureField secureField = (SecureField)field;
secureField.AllowedPrincipals = AllowedPrincipalsPeoplePicker.ResolvedEntities;
secureField.SaveCustomProperties();
Custom Field Control
Custom Field Typeの最後のコンポーネントがCustom Field Controlです。ここでData Storage Listに格納されてるデータを作成したり管理するロジックを実装しますが、それに加えてルックアップフィールドの機能も利用するために LookupFiledクラスを継承します。照会モードでは、LookupFieldクラスが全ての機能を担います。しかし新規作成モードおよび編集モードについては、私たちの要求を満たすようにコントロールに変更を加えます。つまり対象となるフィールドコントロールが現在どのモードにあるかを決めるために、ControlModeプロパティを基底クラス(LookupField)に加えるのです。
新規作成モードあるいは編集モードの場合、そのフィールドコントロールはデフォルトとは違い次のような振る舞うことになります。
- もしユーザが対象となる列についての権限を持ってない場合、そのコントロールを非表示にする
- 値を持つリストアイテムに対しては、テキストボックスを表示してそれがユーザの操作対象になるようにする。
- そのテキストボックスに値が入ったり変更されたりした場合は、リストアイテムの値も変更する。
コントロールの可視を操作するにはVisibleプロパティをオーバーライドするのが一番の方法です。今回はプロパティのgetterでユーザがデータへのアクセス権があるかをチェックし、ない場合はfalseを返すように実装しました。
ユーザがデータを入力あるいは編集できるようにテキストボックスを表示することについては、id TextFieldを使った既存のSharePointテンプレートを使うことができます。これは標準のテキストフィールドで使われているのと同じテンプレートです。これを可能にするにはDefaultTemplateNameプロパティのgetメソッドを次のように実装するだけです。
// If the mode is Display default to Lookup Field functionality
if (ControlMode == SPControlMode.Display || ControlMode == SPControlMode.Invalid)
{
return base.DefaultTemplateName;
}
return @"TextField";
残った機能、Data Storage Listの作成・更新を実装するには、Valueプロパティのgetメソッドとsetメソッドをオーバーライドする必要があります。getメソッドは SharePointフレームワークがフィールド値を更新するのに使われます。今回はユーザによって値が入力された時にData Storage List内でテキストボックスの後ろにあるアイテムを作成したり編集したりするようにロジックを変更します。この機能はユーザがリストの編集で権限にひっかからないように管理者レベル権限で実行します。同時にこのフィールドを編集できてはいけないユーザが編集できないようにするためのロジックも実装します。このように実装したコードはこの章の最後に載せます。
// If the mode is Display default to Lookup Field functionality if (ControlMode == SPControlMode.Display || ControlMode == SPControlMode.Invalid)
{
return base.Value;
}
this.EnsureChildControls();
// Validate the current users permissions.
if (!DoesUserHavePermissions())
{
return lookupListItemId;
}
// Check for an existing value to determine if we create new or edit.
if (lookupListItemId == null)
{
SPSecurity.RunWithElevatedPrivileges(createLookupListItem);
}
else
{
SPSecurity.RunWithElevatedPrivileges(updateLookupListItem);
}
return lookupListItemId;
Value プロパティのsetメソッドはフィールドの現在の値を現在のList Itemにセットするのに使われます。そのために今回はData Storage Listから現在の値を取得してそれをテキストボックスへセットするように実装します。同時にそのデータについてのセキュリティも守られるようにもします。
// If the mode is Display default to Lookup Field functionality
if (ControlMode == SPControlMode.Display || ControlMode == SPControlMode.Invalid)
{
base.Value = value;
return;
}
this.EnsureChildControls();
// Validate the current users permissions.
if (!DoesUserHavePermissions())
{
return;
}
if( value != null)
{
if (value is SPFieldLookupValue)
{
SPFieldLookupValue fullValue = value as SPFieldLookupValue;
lookupListItemId = fullValue.LookupId;
this.TextBoxValue.Text = fullValue.LookupValue;
}
else
{
if (!(value is string))
{
throw new ArgumentException();
}
try
{
SPFieldLookupValue fullValue = new SPFieldLookupValue(value as string);
lookupListItemId = fullValue.LookupId;
this.TextBoxValue.Text = fullValue.LookupValue;
}
catch (ArgumentException ex)
{
this.TextBoxValue.Text = string.Empty;
}
}
以下のメソッドは上記のコードで使われているヘルパメソッドです。
private bool DoesUserHavePermissions()
{
bool doesUserHavePermissions = false;
SPSecurity.RunWithElevatedPrivileges(delegate()
{
SPFieldLookup lookupField = this.Field as SPFieldLookup;
using (SPSite site = new SPSite(SPContext.Current.Site.ID))
{
using (SPWeb web = site.OpenWeb(lookupField.LookupWebId))
{
SPList list = web.Lists[new Guid(lookupField.LookupList)];
SPListItem subFolderItem = SecureField.GetOrCreateSubFolderItem(web, list, ListId, Field, false);
if (subFolderItem == null)
{
throw new Exception("Cannot find the List folder or Field folder.");
}
doesUserHavePermissions = subFolderItem.DoesUserHavePermissions(SPContext.Current.Web.CurrentUser, SPBasePermissions.ViewListItems);
}
}
});
return doesUserHavePermissions;
}
private void createLookupListItem()
{
SPFieldLookup lookupField = this.Field as SPFieldLookup;
using (SPSite site = new SPSite(SPContext.Current.Site.ID))
{
using (SPWeb web = site.OpenWeb(lookupField.LookupWebId))
{
SPList list = web.Lists[new Guid(lookupField.LookupList)];
SPListItem subFolderItem = SecureField.GetOrCreateSubFolderItem(web, list, ListId, Field, false);
if (subFolderItem == null)
{
throw new Exception("Cannot find the List folder or Field folder.");
}
// Create the list item.
SPListItem listItem = list.Items.Add(subFolderItem.Folder.ServerRelativeUrl, SPFileSystemObjectType.File, this.TextBoxValue.Text);
listItem[SecureField.secureFieldStorageFieldName] = this.TextBoxValue.Text;
web.AllowUnsafeUpdates = true;
listItem.Update();
lookupListItemId = listItem.ID;
}
}
}
private void updateLookupListItem()
{
if (lookupListItemId == null)
{
return;
}
SPFieldLookup lookupField = this.Field as SPFieldLookup;
using (SPSite site = new SPSite(SPContext.Current.Site.ID))
{
using (SPWeb web = site.OpenWeb(lookupField.LookupWebId))
{
SPList list = web.Lists[new Guid(lookupField.LookupList)];
SPListItem listItem = list.GetItemById((int)lookupListItemId);
listItem[SecureField.secureFieldStorageFieldName] = this.TextBoxValue.Text;
web.AllowUnsafeUpdates = true;
listItem.Update();
}
}
}
デプロイ
コア機能を拡張してカスタマイズできるというSharePointの開発能力と同じように、SharePoint Soluctionのパッケージも理想的なデプロイツールとなっています。このフレームワークによって、全てのサーバに一貫性と監視体制のあるパッケージを1カ所からデプロイできるパッケージが作成できます。今回の列レベルセキュリティの実装ファイルはコンパイルされたコードファイル、XML設定ファイル、コントロールテンプレートファイルからなりますが、このパッケージはSolutionパッケージに完全に適合したものになっています。
完成版
今回のコンポーネントを別のコンポーネントと組み合わせて使うことで、図4のようにユーザが標準のSharePointインターフェースで保護列を作成することができるようになります。
図4 保護列の作成
保護列を作成できるようになったのに関連して、ユーザはその列へのアクセス権を持たせるユーザおよびグループを選択できます。この選択には図5のようにSharePoint標準の「People Picker」コントロールを使っています。
図5 保護列への権限の設定
この列が作成されて設定が終わると、図6のように権限を持ったユーザであればSharePoint標準の新規作成および編集のフォームを使ってデータを追加することができるようになります。
図6 保護列の値を編集
この列への権限を持たないユーザは編集もデータを見ることもできません。図7がその時の画面です。
図7 保護列へのアクセス権限を持たないユーザでの編集モード
この列が追加されたSharePoint内のビューでも、この列へのアクセス権限を持つユーザだけがアクセスできます。図8と図9がその様子を表した画面です。
図8 保護列へのアクセス権限のあるユーザが開いた時のリスト
図9 保護列へのアクセス権限のないユーザが開いた時のリスト
まとめ
この記事ではSharePointを拡張して列レベルセキュリティを可能にしながらもSharePointの全てのデータもそのまま使えるようにする方法を見てきました。この記事にある方法は、どのSharePoint環境に対してもシームレスに追加することができます。動作可能なサンプルの全ソースコードとデプロイファイルをMSDN Code Galleryのここ(リンク)で公開しています。
この記事にある方法は十分スケーラビリティとセキュリティを備えていると信じていますが、広範囲にわたるテストはしていませんし、ゆくゆくは MicrosoftがSharePointを標準で列レベルセキュリティがあるものにしてくれることを大いに期待しています。その時はUIももっとよくなるでしょうし、私たちのサンプルでははっきりとは裏付けをしていないパフォーマンスやスケーラビリティについても優れたものになることでしょう。
なお今回例にあげた中で触れていない問題には次のようなものがあります。
- 親オブジェクトがなくなった時に保護レコードを消去する
- テキスト以外の列の型を使えるようにする
- 列についての照会と編集の権限を分けること
列の追加機能についてデータシートビューでの検証をおこなうこと
著者について
Matthew Dressel氏は8年以上にわたって最新のMicrosoft技術を使う組織・企業のためにシステムの設計および実装をおこなってきました。氏が現在取り組んでいるのは、MicrosoftのSharePoint関連技術の導入に目を向けている教育分野のクライアント向けのシステムのアーキテクチャやプロジェクトのリーダです。氏はMicrosoft SharePointプラットフォームに3年以上取り組んでいます。その間にはSharePointの初期の実装や現在のバージョンへの移行を経験しています。「標準的な」SharePointの導入を数多く成功させてきたのに加え、最近は新しい製品あるいはサービスの基盤としてSharePointプラットフォームを使ういくつものプロジェクトに関わっています。これらのプロジェクトでは入念なアーキテクチャとプラットフォームの複雑やパワーについての深い見識によって独自のコードや処理を活用しています。
Grzegorz Gogolowicz氏はMicrosoftのグローバル・パートナ・アーキテクチャ・チームの上級ソリューションアーキテクトで、15年以上のソフトウェア開発の経験があります。以前はMicrosoftのVisual Studioチームシステム製品群のテクニカルリードとしてTeam Foundation Serverを中心に関わっていました。現在のポジションでは、.NET、SharePoint、そしてAzure Services Platformを含むアプリケーションプラットフォームのアーキテクチャを専門にしています。
その他の情報
詳細な情報については以下のリソースを参照してください。:
- Custom Field Types(リンク)
- Users, Groups, and Authorization(ユーザ、グループ、認証)(リンク)
- Plan site and content security (Windows SharePoint Services)(サイトおよびコンテンツのセキュリティを考える(Windows SharePoint Services))(リンク)
- Plan site and content security (Office SharePoint Server)(サイトおよびコンテンツのセキュリティを考える(Office SharePoint Server))
- Microsoft Office Developer Center (リンク)
原文はこちらです:http://www.infoq.com/articles/Dressel-Gogolowicz-wss-security
(このArticleは2008年11月24日に原文が掲載されました)