Monday, November 28, 2005

List-Level Permissions in SharePoint Portal Server


This may just be the most unsupported tip I have ever posted, and for someone who is known for finding creative ways to enable or implement features that often fall into the 'unsupported' category, that's saying quite a bit. I struggled with whether or not to post this for a few days but in the end I decided that the value outweighs the cost. You're all big boys and girls so you can make your own decisions on whether or not to implement this solution. If you do, you are completely on your own - although I've tested it in at least three scenarios without incident, that is no guarantee that it won't completely crash your portal implementation. So...


Now that the disclaimers are out of the way (this is serious - don't do this if you're uncertain of how it will effect your portal), let's get down to business. One of the most frustrating restrictions in SPS is the inability to assign list-level permissions to individual lists. Unlike WSS, SPS lists inherit permissions from the portal area they live in. This means that you must create a new subarea in order to implement individual list ACL's.

This limitation is enforced by the ExternalSecurityProvider value specified in the ONET.XML file for each site definition. According to the SDK, this COM GUID, which is static for all SPS implementations, defines the security context for the search crawler. Strangely enough, it is also referenced on line 522 of the listedit.aspx file, which obviously has nothing to do with search and indexing. When present, the 'Change Permissions for this List' link is hidden; when removed, it is displayed. By commenting out the associated 'if' statement (lines 522 through 537), the 'Change permissions...' link becomes active for all lists; clicking on it takes the user to the Change Permissions page (shropt.aspx).

Unfortunately, disabling the SecurityProvider check is not enough to enable list-level permissions. As one will quickly discover, clicking on the 'Add Users' button on any subarea (but not the root SPS area) will lead to an endless authentication loop. This confounded me as it seemed to work fine in the SPS definition but nowhere else. When the same issue came up with Jan Tielens' UserAlerts web part and some custom web parts a colleague was developing, I realized they were all related.

The difference is due to a small change to the ExternalSecurityProvider GUID in the ONET.XML file of the SPS site definition and all other definitions (STS, you will note, does not have such a setting). At the bottom of SPS ONET.XML file you will find the following entry:

<Components> <FileDialogPostProcessor ID="C6659361-1625-4746-931C-36014B146679" /> <ExternalSecurityProvider ID="A373E6A7-7A87-11D3-B1C1-00C04F68155C" Type="Microsoft.SharePoint.Portal;Microsoft.SharePoint.Portal.SiteData.
CategoryWebSecurityProvider" /></Components>

Look closely at the ExternalSecurityProvider ID value. For most of the other Site Definitions the first octet ends in an '8' instead of '7' (i.e. A373E6A8 instead of A373E6A7). Looking into the WEBS table of the _SITE database, I discovered that this GUID is stored in the SecurityProvider column for each area (interestingly, bucket areas have a '9' at the end of the first octect instead of a '7' or an '8'). Apparently, whenever the DLL retrieves this value from the DB, if it does not match a certain value range, access to the function being called is not allowed, which results in the authentication loop. At first, I assumed that this only applied to list permissions as listedit.aspx is the only file in which the ExternalSecurityProvider setting is referenced; however, I discovered that several object-model queries (user.alerts and user.roles) also share the same restrictions. Any code that accesses these methods from a portal subarea will fail.

The solution to these problems is simple yet potentially dangerous (if you are still reading now would be a good time to bail out). It involves modifying the listedit.aspx page (not supported), changing the GUID value in the ONET.XML file for your custom site definition (OK as far as I can tell - it's your definition, after all), and, if any areas have already been created, updating the WEBS table with the new GUID value (definitely not supported).
For the bold and brave, here's the procedure:

1. Modify the 'listedit.aspx' file by commenting out (//) lines 522 - 537, as follows:

// if (!spWeb.HasExternalSecurityProvider)// {%> <TR> <TD> </TD> <TD class=ms-propertysheet colspan=4><IMG SRC="/_layouts/images/rect.gif" alt=""> <A ID=onetidListEdit7 target=_self <% if (iBaseType == SPBaseType.DocumentLibrary) { %> HREF="ShrOpt.aspx?obj=<%SPEncode.WriteUrlEncode(Response, spList.ID.ToString("B").ToUpper());%>,doclib" <% } else { %> HREF="ShrOpt.aspx?obj=<%SPEncode.WriteUrlEncode(Response, spList.ID.ToString("B").ToUpper());%>,list" <% } %> ><% switch (iBaseType) { case SPBaseType.DocumentLibrary: if (SPListTemplateType.PictureLibrary == iServerTemplate) { %>Change permissions for this picture library<% } else if (SPListTemplateType.XMLForm == iServerTemplate) { %>Change permissions for this form library<% } else if (SPListTemplateType.ListTemplateCatalog == iServerTemplate SPListTemplateType.WebTemplateCatalog == iServerTemplate SPListTemplateType.WebPartCatalog == iServerTemplate) { %>Change permissions for this gallery<% } else { %>Change permissions for this document library<% } break; case SPBaseType.DiscussionBoard: %>Change permissions for this discussion board<% break; case SPBaseType.Survey: %>Change permissions for this survey<% break; default:%>Change permissions for this list<% break; }%></A></TD></TR><%// }

2. Change the first octet of GUID value in ONET.XML file from 'A373E6A8' to 'A373E6A7', as follows:

<ExternalSecurityProvider ID="A373E6A7-7A87-11D3-B1C1-00C04F68155C" Type="Microsoft.SharePoint.Portal;Microsoft.SharePoint.Portal.SiteData.
CategoryWebSecurityProvider" />

3. Run the following SQL statement on the SITE database in SQL Query Analyzer:

UPDATE Webs SET SecurityProvider = '{A373E6A7-7A87-11D3-B1C1-00C04F68155C}' WHERE SecurityProvider = '{A373E6A8-7A87-11D3-B1C1-00C04F68155C}'

That's it. You now have list-level permissions in portal subareas. As I mentioned, this may have unintended consequences; this value may by used elsewhere and cause something that was working to break or disable some dependent functionality. In my test scenarios I was unable to find any negative effects but that doesn't mean they don't exist. Try it at your own (considerable) risk.

BTW, if anyone from Microsoft reads this and knows what effect a change in the SecurityProvider GUID might have on portal operation, please enlighten us as it is not documented anywhere that I could find.

As an aside, I am certain that this post will officially exclude me from MVP consideration for the rest of my life ;-)