Windows SharePoint Services 3.0和Microsoft Office SharePoint Server 2007支持丰富的安全模型,允许管理员把特定安全对象(比如站点、列表、库、文件夹甚至独立的文档和条目)的权限分配给用户和用户组,来控制对站点和内容的访问。
然而,在某些情形下,需要对列表和文档库中单独的列进行安全控制。目前,SharePoint没有为列或视图的安全控制提供内置支持。一个需要这种功能的典型场景就是,在一个包含了大量雇员或客户的信息的列表当中,某些特定的列(薪资、工资发放日期、晋升机会等等)最好只被这个门户里的特定用户组查看到。
为了满足这些场景的要求,本文描述了一个利用SharePoint扩展性和内置条目级别安全性的方法,允许在自定义字段类型上实现列基本权限控制。这是通过使用一个查询字段作为列来实现的,在这种方式的背后实际上紧密关联着另外一个列表,其中包含了安全值和只为具有有效权限的用户返回这些值的方法。
其结果就是,在视图模式中经授权的用户将看到安全列的内容,就像它是一个普通列一样;相反,未经授权的用户就根本看不到这个列的内容。在图1中显示了不同的行为。类似的,只有经授权的用户能够以新建和编辑模式访问安全列的内容。
图1:经授权和未经授权用户查看安全列的效果
为了设计自定义列级安全性解决方案,需解决如下问题:
为了解决这些问题,我们决定使用自定义字段类型(Custom Field Type)用于数据的呈现和处理,并使用条目级别安全性(Item Level Security)的功能用于数据存储。
自定义字段类型同内容类型(Content Types)和列表窗体(List Forms)是Windows SharePoint Services的主要扩展机制,允许自定义数据的访问、呈现和处理。你能在Windows SharePoint Services 3.0 SDK中阅读关于自定义字段类型的内容。
我们的自定义字段类型的主要角色是保证只有具有正确授权的用户才能查看或编辑数据。我们实现了一个名为Secure Column的自定义字段类型,能用于任何SharePoint列表中。
对于存储数据,我们考虑过的一种选择是在单独的数据库中使用单独的数据表。这对于为了其他目而使用单独数据库的情况来说,是一种较好的 SharePoint实现选择。然而,考虑到大部分SharePoint解决方案都未使用非SharePoint数据库,所以针对这篇文章和示例,我们决定使用SharePoint列表(List)作为存储机制,而不引入对单独数据库的依赖。
使用SharePoint列表作为安全数据的后端存储方式,能够为每条需要进行安全控制的数据创建一个列表条目(List Item)。为了提供适当的安全保证级别,条目级别安全(Item-Level Security)特性也要被使用到。我们在这里承诺,列级安全性应该和内置的SharePoint条目级别安全性一样安全。
对于实现决策,要着重注意的一点就是,使用SharePoint列表和查询(Lookup)列功能同样具有一些固有的限制。这些限制是:在非常大的列表中进行查找会影响到性能,并且查询列也不能直接访问位于不同站点集合(Site Collection)中的列表。
为了实现列级安全性,需要几个自定义组件:
这些组件如图2所描述的那样,协同工作在一起,以允许用户创建、查看和编辑存储在安全列中的数据。
图2 实现概要
对于这个示例实现,一个单独的数据存储列表(Data Storage List)用于保存这个站点集中的所有数据。这样可以让这个解决方案更简单,更易维护。不过一个缺点就是,所有的安全字段都依赖于单一的存储点。在非常庞大的解决方案中,这可能会引起性能问题。如果出现这种情况,那么示例解决方案也能够扩展成为每个安全字段创建一个新的数据存储列表。
虽然使用SharePoint列表作为安全数据的存储方式带来了很多好处,如条目级别安全性和完整的API支持。但也需要面对几个挑战:
这些挑战中的每一个都需要用标准的SharePoint列表功能或通过添加自定义字段类型中的自定义代码来解决。
为了解决可见性的问题,我们创建了“目录”列表。这类似于创建标准SharePoint实现中的那些系统列表。目录列表包括:Web部件库、站点模板库和母版页库。另外,SPList对象的如下属性也进行了设置以进一步隐藏列表:
为了解决可伸缩性和权限维护问题,我们决定不使用平面表,而是转而创建一个两级文件夹结构来分组这些条目。这个文件夹结构的第一级和给定的安全列所属的列表相对应。第二级和安全列本身相对应。最终,在一个单独文件夹中,条目数量就不会超过给定安全列所属列表的条目数量,即使对于那种包含了多个安全列的列表也是如此。这种方式能够解决可扩展性的问题。拥有这样的文件夹结构,我们就可以使用第二级文件夹的权限来表示和给定的安全列关联的权限。这样也去除了为每个独立条目维护权限的需要。
图3:文件夹结构
最后一个问题是创建出的这个列表应该能作为自定义字段类型的一部分被轻易地处理。为了达到这个目的,假如当新字段创建的时候不存在这个列表,我们就用代码来创建它。
在上面的解决方案架构一节提到,数据不是真正的存储在这个列表中,而是通过添加到SharePoint列表中的安全列在逻辑上的表示。实际情况是,把数据安全地存储在独立的、专门的SharePoint列表中,就是我们提到的数据存储列表(这个列表具有一个叫做_SecureFieldStorage的内部名称,并且具有叫做_SecureFieldStorage 的站点相对URL)。获取存储在这个独立的数据存储列表中的数据,并把其显示在宿主列表的上下文中,则是自定义字段类型的主要用处。
当我们在研究如何实现这个功能的时候,我们希望让我们的自定义字段类型从基础的SPField类继承以避免从零开始。我们应尽可能地利用 SharePoint现有的功能。如果我们考虑把一些可用的SharePoint现有的字段类型用作我们自定义字段类型的基类时,查询字段类型(SPFieldLookup)会是最好的选择。
对于查询字段,我们打算使用的主要特性是指向其他SharePoint列表中的字段,并基于特定的列表条目编号获取这个字段的值,这就是我们所需要的功能。而且,使用查询字段功能允许充分利用内部实现的一些优点——它们得利于SQL Join的功能,在提供很好的伸缩性的同时还能具备很好的安全性。
虽然查询字段类型的核心功能为我们所需功能提供了很好的开端,不过在标准查询字段特性和我们的需求之间还存在很多不匹配的地方。这就是我们的自定义代码的用武之处——用于扩展SPLookupField:
任何自定义字段类型实现的一个重要组件就是自定义fldtypes.xml文件。在我们的这个例子中,这个文件被定制为指向我们的自定义字段类型类:
<FieldName="FieldTypeClass"> SecureField.SecureField, SecureField, Version=1.0.0.0, Culture=neutral, PublicKeyToken=48a15d1316dd0f7d Field>
我们也包含了一个自定义显示模式。这个显示模式采用如下方式被识别,并且保证它不会被呈现为超链接,它默认用于Lookup列。
<LookupColumn HTMLEncode ="TRUE" "AutoHyperLink="FALSE""/>
我们仅仅定义的另外一个是设定一个自定义字段编辑器控件。
<FieldName="FieldEditorUserControl">/_controltemplates/SecureFieldEditor.ascx Field>
自定义字段类型类包含了我们自定义的核心查询字段功能的主要代码。这些功能包括:
这个类从SPFieldLookup继承,并重写了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);
}
除了核心的字段类型类之外,还需要一个自定义字段编辑器控件,才能让用户设置安全列上的权限。权限不仅能在列被添加到列表的时候设置,也可以之后随时更新。这个功能是用一个包含了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();
自定义字段类型的最后一部分是自定义字段控件,它实现了创建和维护存储在数据存储列表中的数据的逻辑。为了达到这个目的,且依旧保持现存查询字段的功能,我们让自定义类从LookupField类继承。在字段处于显示模式的时候,这个基类处理所有的功能。然而,在新建和编辑模式下,我们要修改一些地方让这个控件符合我们的需要。目前,我们使用基类的ControlMode属性确定字段控件处于什么模式下。
当字段控件处于新建或编辑模式下,需要对默认的功能进行几个改变以实现如下行为:
为了控制控件的可见性,最好的方法是重写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";
为了实现创建或更新数据存储列表的余下功能,我们需要重写Value属性的Get和Set方法。Get方法将被SharePoint框架用于更新字段值。我们对于这个逻辑的自定义,是基于用户输入的值创建或编辑位于数据存储列表中的后台条目。为了避免没有权限的用户编辑列表,在必要时,该功能将运行在比较严格的安全级别下。我们也实现了保证用户不会对不能编辑的字段进行编辑的逻辑。在代码中,我们引用了多个辅助方法。这些方法的代码在本节的最后也能找到。
// 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方法用于为当前列表条目设置字段的值。我们针对这个功能的定制需要从数据存储列表中获取当前值,并显示到文本框中。它也实现了保证数据安全性的功能。
// 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 Solution包是最佳的部署工具。这个框架允许我们创建部署安装包,以中心的、一致的和可检测的方式部署到服务器群集中的所有服务器上。我们所实现的自定义安全功能包含了一个程序集,一个XML配置文件和一个控件模板文件,以及一个大小适中的Solution包。
把这些组件彼此结合,用户就可以使用如图4所示的标准SharePoint界面来添加安全列了。
图4:创建安全列
作为添加列的一部分,用户能够选择哪些用户和用户组能够对这个列进行访问。这一功能使用了标准的SharePoint“People Picker”控件,如图5所示。
图5:为安全列配置权限
一旦列添加好,并且配置了具有相应的访问权限的用户,那么列就可以使用标准的SharePoint新建和编辑窗体来添加数据了,如图6所示。
图6:编辑安全列的值
对这个列不具有权限的用户就不允许编辑或查看这个列当中的数据。从图7中可以看到。
图7:当用户不具有编辑安全列的权限时编辑模式的显示效果
当这个列添加到SharePoint中的一个视图中时,只有拥有该权限的用户能够访问。可以从图8和图9中看到。
图8:具有安全列权限的用户查看列表的效果
图9:不具有安全列权限的用户查看列表的效果
本文展示了如何扩展SharePoint以包含列级别安全性的功能,并在SharePoint内部保存所有数据。在本文中被验证的对策能够让这个功能无缝地附加到任何SharePoint环境中。另外,我们也在MSDN Code Gallery上发布了一个可以运行的例子的完整的源代码及部署文件。
尽管我们相信本文所演示的方式能充分地保证可伸缩性,并且是安全的,但我们没有进行广泛的测试;并且我们非常希望,如果微软能在未来的 SharePoint版本中包含内置的列基本安全性功能,那么它将会比我们的示例具有更好的UI、更好的性能和可伸缩性,而且它也可以有售后支持,而对于这一点,很显然我们的示例无法做到。
在我们的例子中未被解决但已得到验证的其他事情还有:
应该考虑一下在数据表视图中,如何改善列的功能。