Wednesday, November 24, 2010

XsltListViewWebPart – how to display columns based on permissions

The standard SharePoint security trimming in the XsltListViewWebPart works on the list item level – the items in the displayed SharePoint list may inherit the permissions from their parent list, but may also have different permissions and based on these the viewing user depending on his rights may see different result sets. In most cases this “horizontal” security trimming will be sufficient for the end users, but there may be cases when a “vertical” type of security trimming is also requested – meaning that certain columns in the list view are displayed only to users with particular rights for the target SharePoint list.

So, I created a small “proof-of-concept” type solution that demonstrates how this can be achieved. The implementation is fairly simple and involves only changing of the XSL used by the web part and modifying its “PropertyBindings” property. The modifications of the rendering XSL are obviously the core part of the implementation but you may wonder what the purpose of using the “PropertyBindings” property of the XsltListViewWebPart is (if you are not familiar with the “PropertyBindings” property and how to use it, you can check out my extensive posting on the subject here). The idea is simple – in the “PropertyBindings” property you can keep configuration information that also becomes available in the XSL of the web part. And the configuration data that is needed in our case is the permissions’ data for the list columns that the web part displays – it is about what rights exactly the user should have for the target list, so that he can see one or another column in the list view. The question here is why put this configuration in the web part’s property bindings and not in the fields’ schema itself for example (the field schema can easily be extended with custom attributes – e.g. xmlns:PermissionMask="0x7FFFFFFFFFFFFFFF"). The main reason for this is that if you use custom attributes in the field schema XML, you cannot then use them in the rendering XSL of the XLV web part, custom attributes simply don’t appear in the view schema XML that is available through the “$XmlDefinition” XSL parameter (check this MSDN article for a sample view schema XML in the XLV’s XSL – the field schema data is in the View/ViewFields/FieldRef elements). Another point here is that even if it were possible to store the field permission data in the field’s schema, this would impact all list views that use the customized XSL (and display the particular column), and with the setting of the “PropertyBindings” property only the current XLV web part will be affected. It is hard to judge whether this is of advantage or disadvantage  and probably depends on the specific case that you may have.

And let me first show you the “PropertyBinding” XML that should be added to the “PropertyBindings” property (I said “added”, because the property normally already contains the XML of other property bindings):

<ParameterBinding Name="ColumnPermissions" DefaultValue="Author|9223372036854775807|Editor|756052856929" />

The “Name” attribute specifies the name of the xsl:param element that will be initialized with the value of the property binding in the web part’s XSL. Its value is in the “DefaultValue” attribute – it is a long string containing values delimited by the ‘|’ character. At odd positions you have field names (internal names actually) and at even positions you see big numbers, which are actually the permission masks that should be applied for the list columns which precede the corresponding number. The permission mask is determined by the standard SharePoint SPBasePermissions enumeration: 9223372036854775807 (hex 7FFFFFFFFFFFFFFF) corresponds to the “Full Control” permission level and 756052856929 (hex B008431061) corresponds to the “Contribute” permission level. This means that the user will see the “Author” (“Created by”) column only if he has “Full Control” rights and the “Editor” (“Modified by”) column only if he has “Contribute” rights for the SharePoint list that is displayed. Note that all fields that are not specified in the property binding will be always visible to all users.

Let’s now move to the custom XSL that should handle the rendering and display only the columns that the current user has rights to see. Before, I show you the complete source of the custom rendering XSL, I want to draw your attention to one important thing – the trick that is used in the XSL to check whether the permission masks for the fields have a match with the effective permissions of the current user for the source SharePoint list. It is very simple actually and uses … a standard SharePoint “ddwrt” XSLT extension method:

<xsl:if test="ddwrt:IfHasRights($checkResult)">

The “IfHasRights” extension method receives an integer parameter for the permission mask and returns true or false depending on whether the current user has those rights for the SharePoint list of the web part. Note that the check is made for the SharePoint list, not the items of the list and not for its parent SharePoint site.

And here is the complete source of the custom XSL (check the extensive comments inside it for more details)

<xsl:stylesheet xmlns:x="http://www.w3.org/2001/XMLSchema" xmlns:d="http://schemas.microsoft.com/sharepoint/dsp" version="1.0" exclude-result-prefixes="xsl msxsl ddwrt" xmlns:ddwrt="http://schemas.microsoft.com/WebParts/v2/DataView/runtime" xmlns:asp="http://schemas.microsoft.com/ASPNET/20" xmlns:__designer="http://schemas.microsoft.com/WebParts/v2/DataView/designer" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:SharePoint="Microsoft.SharePoint.WebControls" xmlns:ddwrt2="urn:frontpage:internal">

 

  <!-- import the standard main.xsl, so we have all standard stuff -->

  <xsl:import href="/_layouts/xsl/main.xsl"/>

  <xsl:output method="html" indent="no"/>

 

  <!-- we get here the field permissions configuration from the PropertyBinding with the same name -->

  <xsl:param name="ColumnPermissions" />

  <!-- this is the standard XmlDefinition parameter - the XLV initializes this one with the view schema -->

  <xsl:param name="XmlDefinition" />

 

  <!-- this variable contains the parsed configuration data like <token>Author</token><token>9223372036854775807</token> etc -->

  <xsl:variable name="tokens">

    <xsl:call-template name="Tokenize">

      <xsl:with-param name="string" select="$ColumnPermissions" />

      <xsl:with-param name="delimiter" select="'|'" />

    </xsl:call-template>

  </xsl:variable>

 

  <!-- here we create a copy of the original XmlDefinition removing all View/ViewFields/FieldRef elements for which the user doesn't have rights -->

  <xsl:variable name="XmlDefinition2Raw">

    <xsl:apply-templates mode="transform-schema" select="$XmlDefinition" >

      <xsl:with-param name="tokenSet" select="msxsl:node-set($tokens)" />

    </xsl:apply-templates>

  </xsl:variable>

 

  <!-- the one above is a sequence of tags, in order that it can be used exactly like the standard $XmlDefinition it should be converted to a node set -->

  <xsl:variable name="XmlDefinition2" select="msxsl:node-set($XmlDefinition2Raw)" />

 

  <!-- this one is simply a copy of the template with the same match from the standard vwstyles.xsl (thus we override it), the only difference is that it uses our trimmed $XmlDefinition2 instead of the standard $XmlDefinition -->

  <xsl:template match="/">

    <xsl:choose>

      <xsl:when test="$RenderCTXOnly='True'">

        <xsl:call-template name="CTXGeneration"/>

      </xsl:when>

      <xsl:when test="($ManualRefresh = 'True')">

        <xsl:call-template name="AjaxWrapper" />

      </xsl:when>

      <xsl:otherwise>

        <xsl:apply-templates mode="RootTemplate" select="$XmlDefinition2"/>

      </xsl:otherwise>

    </xsl:choose>

  </xsl:template>

 

  <!-- the same as the template above -->

  <xsl:template name="AjaxWrapper" ddwrt:ghost="always">

    <table width="100%" border="0"  cellpadding="0" cellspacing="0">

      <tr>

        <td valign="top">

          <xsl:apply-templates mode="RootTemplate" select="$XmlDefinition2"/>

        </td>

        <td width="1%" class="ms-vb" valign="top">

          <xsl:variable name="onclick">

            javascript: <xsl:call-template name="GenFireServerEvent">

              <xsl:with-param name="param" select="'cancel'"/>

            </xsl:call-template>

          </xsl:variable>

          <xsl:variable name="alt">

            <xsl:value-of select="$Rows/@resource.wss.ManualRefreshText"/>

          </xsl:variable>

          <a href="javascript:" onclick="{$onclick};return false;">

            <img src="/_layouts/images/staticrefresh.gif" id="ManualRefresh" border="0" alt="{$alt}"/>

          </a>

        </td>

      </tr>

    </table>

  </xsl:template>

 

  <!-- this template creates the copy of the standard $XmlDefinition trimming the View/ViewFields/FieldRef elements for which the user doesn't have rights -->

  <xsl:template mode="transform-schema" match="View" >

    <xsl:param name="tokenSet" />

    <!-- copy the root View element -->

    <xsl:copy>

      <!-- copy the root View element's attributes -->

      <xsl:copy-of select="@*"/>

      <!-- copy the child elements of the root View element -->

      <xsl:for-each select="child::*">

        <xsl:choose>

          <xsl:when test="name() = 'ViewFields'">

            <!-- special handling of the ViewFields element -->

            <ViewFields>

              <!-- iterate the ViewFields/FieldRef elements here -->

              <xsl:for-each select="child::*">

 

                <!-- get the permission mask for the FieldRef element, by the Name attribute -->

                <xsl:variable name="checkResult">

                  <xsl:call-template name="GetValueFromKey">

                    <xsl:with-param name="tokenSet" select="$tokenSet" />

                    <xsl:with-param name="key" select="./@Name" />

                  </xsl:call-template>

                </xsl:variable>

 

                <xsl:choose>

                  <!-- if the permission mask is not empty and the ddwrt:IfHasRights returns true, copy the field -->

                  <xsl:when test="$checkResult != ''">

                    <!-- this is how we check whether the user has sufficient rights for the field (checking the permission mask of the field against the user's permissions for the source list) -->

                    <xsl:if test="ddwrt:IfHasRights($checkResult)">

                      <xsl:copy-of select="."/>

                    </xsl:if>

                  </xsl:when>

                  <xsl:otherwise>

                    <!-- if we don't have the field in the configuration simply copy the FieldRef element -->

                    <xsl:copy-of select="."/>

                  </xsl:otherwise>

                </xsl:choose>

 

              </xsl:for-each>

            </ViewFields>

          </xsl:when>

          <xsl:otherwise>

            <xsl:copy-of select="."/>

          </xsl:otherwise>

        </xsl:choose>

      </xsl:for-each>

    </xsl:copy>

  </xsl:template>

 

  <!-- several helper templates that parse the configuration string and return the permission mask for the field by providing the field's internal name -->

  <xsl:template name="GetValueFromKey">

    <xsl:param name="tokenSet" />

    <xsl:param name="key" />

    <xsl:apply-templates select="$tokenSet/token[text() = $key]" />

  </xsl:template>

 

  <xsl:template name="NextNode" match="token">

    <xsl:value-of select="following-sibling::*"/>

  </xsl:template>

 

  <xsl:template name="Tokenize">

    <xsl:param name="string" />

    <xsl:param name="delimiter" select="' '" />

    <xsl:choose>

      <xsl:when test="$delimiter and contains($string, $delimiter)">

        <token>

          <xsl:value-of select="substring-before($string, $delimiter)" />

        </token>

        <xsl:call-template name="Tokenize">

          <xsl:with-param name="string" select="substring-after($string, $delimiter)" />

          <xsl:with-param name="delimiter" select="$delimiter" />

        </xsl:call-template>

      </xsl:when>

      <xsl:otherwise>

        <token>

          <xsl:value-of select="$string" />

        </token>

        <xsl:text> </xsl:text>

      </xsl:otherwise>

    </xsl:choose>

  </xsl:template>

 

</xsl:stylesheet>

One note about the XSL code – if you have a look at it, you will notice that it replaces (overrides) two of the core XSL templates used in the standard vwstyles.xsl file. It then provides a modified copy of the standard view schema XSL parameter ($XmlDefinition) in these templates. And on the other hand if you check the vwstyles.xls file, you will notice that there are still other XSL templates in it that also use the standard $XmlDefinition parameter (and thus not the modified copy) – these are the templates that handle aggregations and groupings, which means that the customized XSL above won’t be able to handle properly these cases.

And finally a few words on how to use this sample: the first thing is to save the XSL to a XSL file in the TEMPLATE\LAYOUTS or TEMPLATE\LAYOUTS\XSL folder (a subfolder of these two is also possible). Then you need to select your XLV web part (it may be an XLV from a standard list view page or an XLV that you placed on a content page) and change its PropertyBindings and XslLink properties. You can do that with code or using my web part manager utility which provides an easy to use UI for that (you can download it from here). For the “PropertyBindings” property, you should append the XML of the field permissions configuration which should look like the sample above. In the “XslLink” property you should specify the link to the XSL file in which you have saved the custom XSLT above – provided you’ve saved the XSL file as TEMPLATE\LAYOUTS\columns_main.xsl, the value that you should put in the “XslLink” property should be: /_layouts/columns_main.xsl.

13 comments:

  1. Thanks for this. Worked like a charm.
    I tried to run some code using SPSecurity.ElevatedPrivileges() but didnt work as expected. Will post the same below incase some one decides to go down this path:

    site = new SPSite(SPContext.Current.Site.ID, SPUserToken.SystemAccount);
    web = site.OpenWeb(SPContext.Current.Web.ID);
    if (web.Lists.TryGetList(ListName) == null)
    {
    DisplayLiteralControl();
    }
    else
    {
    SPList list = web.Lists[ListName];
    XsltListViewWebPart wp = new XsltListViewWebPart();


    // Hook up the list and the view
    wp.ListId = list.ID;
    wp.ListName = list.ID.ToString();
    wp.ViewGuid = list.Views[ViewName].ID.ToString();
    wp.XmlDefinition = list.Views[ViewName].GetViewXml();

    wp.ChromeType = PartChromeType.None;
    wp.ItemContext = SPContext.GetContext(web);

    this.Controls.Add(wp);
    }

    The above code was put into a custom webpart. Lists and corresponding views were available as the webpart properties. Only views prefixed with "Secure_" will be available. This is to allow list owner to specify views that will be available to the custom list view webpart.

    The idea was to remove permissions from the list and only allow access to the list through the custom webpart running under elevated priviliges. However this kept throwing a null reference exception somewhere inside the XsltListView webpart.

    Basically what was happening is when the list view webpart tried to execute the query, it used SPSite and SPweb object from SPContext and not the custom ones that I provided.

    Too bad they sealed the xsltlistview webpart class. I cant think of any other way but it will be good to see if there are other solutions :-)

    Any way thanks for the above post. Much appreciated.

    ReplyDelete
  2. What if the user will access the list using another Office software (Access, Excel, SP Workspace)? He will see data from all the columns.

    ReplyDelete
  3. Hi,

    Yes the user will see all columns if he uses any other application. The solution above works only for the XsltListViewWebPart web part.

    Greets
    Stefan

    ReplyDelete
  4. Stefan,

    Great Blog! I have a question & perhaps you can steer me in the right direction. I need to display documents relevant to the current user in a XLV or DVWP. The Doc Lib and User Profile share a managed metadata column and both accept multi-values. These are the columns that I need to join to restrict the view to show relevant docs. So it's like a many to many join and since WP connections only send the first value, I need to customize. I deally this would all happen in an XSL file and configured by XSL link in UI.

    Here's my approach. I have a joined SPDatasource which returns the current user from User Info List by including the UserId in the selectcommand. At this point all docs are being retuned from the library rather than the filtered docs. I think I need to tokenize the user field which is delimited by ';' and for-each loop to display the documents where the related field 'contains' each value. I think I can do all of this in the XSL, but wondered if I should use something similar to above and alter the selectcommand of Docs to use the 'includes' CAML query element instead. Your comments or alternate approach ideas greatly appreciated.

    Thanks, Josh

    ReplyDelete
    Replies
    1. Hi Josh,

      Regarding your second question - I didn't quite get the idea of the managed metadata column (I suppose that this is supposed to work similar to "tags" with having these set to both the documents and the users - users are supposed to see only documents which have at least on of the tags set in the predefined tag set of the current user). If this is the case - your idea with handling this in the XSL is doable .. That being said - I am also thinking of some easier implementation using out of the box stuff - I can think of two possible alternatives here - using the internal SharePoint permission trimming or using SharePoint audiences. In both cases the UI experience for administering this will be different compared to your original idea of using a metadata column. In the first case you can define several SharePoint groups with every user added to some of these groups depending on his preferences and similarly the permissions for the specific documents should be set accordingly for these groups. The administration of this option will definitely require more clicks in the SharePoint UI though. The second option is to use SharePoint audiences - the setup here is pretty similar to the one with the permission groups. Both of these will have the advantage of having the permissions/viewer trimming already in place in the XLV. Another possible advantage is that you can include multiple users simultaneously in a SharePoint group or audience if for instances the users are already part of an AD group or DL or based on a specific profile property.
      One thing which will be harder to achieve here is the option of having the user change his preferences himself - which is obviously very easy with the managed metadata profile property (although as I mentioned audiences can be based on profile properties as well).

      Greetings
      Stefan

      Delete
  5. Stefan,

    I tried using your technique above to alter the View Query, but it doesn't seem to work. Am I doing something wrong or is the Query not alterable the same way fields are due to XSL processor or some such reason?
    See my code here:

    https://gist.github.com/4095951

    Thanks in advance,
    Josh

    ReplyDelete
  6. Hi Josh,

    Unfortunately what you are trying to achieve in the XSL transformation won't work this way - you can definitely modify the Query element of the view schema, but this won't change the CAML used to retrieve the list items. Actually by the time the XLV web part invokes the XSL transformation, the result set is already fetched from the data source (SharePoint list or BCS). I would suggest that you simply modify the query CAML of the XLV's underlying SPView - every XLV has one unique hidden SPView associated with it (check my other postings on the subject), so you will be able to put all your custom filtering logic there without it being necessary to use custom XSL and parameter bindings.

    Greetings
    Stefan

    ReplyDelete
  7. Stefan,

    Thanks for your replies. You are correct: both users and docs are tagged with multiple values. users are supposed to see only documents which have at least one of the tags set in the predefined tag set of the current user. I hope to avoid item level permissions. I explored using Audience Targeting, but it seems filtering is only available when displaying using the CQWP not in XLV. Except for targeting the whole XLV webpart to an audience, but that won't elegantly solve the many-to-many problem since there are too many unique combinations of values.

    I've read your posts about SPView. Can the SPView query be manipulated using client side code? I'm on O365 limited to sandbox, but hoping for UI solution rather than VStudio.
    I've spent forever on this an now I believe I'm beginning to see why there is limited OOB filtering on multi-value fields. Thanks again! Josh

    ReplyDelete
    Replies
    1. Hi Josh,
      Yes you are right about the audience filtering thing being available in the CQWP only - when you mention it now, I remembered the ugly implementation for that in the CQWP - it is being run as a second pass after the first pass site data query fetches the list data (luckily the infamous CQWP will be superseded in SP 2013 by the new content search web part).
      Regarding your question about the possibility of updating the SPView object with the client object model - yes this is possible - I quickly checked this with my SharePoint 2010 explorer tool: http://stefan-stanev-sharepoint-blog.blogspot.nl/2010/05/sharepoint-2010-explorer-using-client.html
      There is a version of it adapted to work with Office 365 (written by a SharePoint fellow guy): http://sharepointrepairjoint.blogspot.com.au/2012/07/sharepoint-2013-explorer.html

      Greetings
      Stefan

      Delete
  8. This comment has been removed by the author.

    ReplyDelete
  9. HI Stefan,


    Your code works like a charm. But my requirement is little different. Instead of hiding the columns, I need to remove the hyperlinks. For ex: if I login as a user with full control, I need to show link for a document library item other wise, I need to remove the hyperlink below the document library. Can you let me know how to modify the xslt to have that kind of behavior

    -Premchand

    ReplyDelete
  10. Hi Premchand,
    You can use the built-in FileLeafRef field which will display the file's name without the hyperlink. Note that the field is hidden and you can't add it to the view through the user interface. You can do that easily with code though. So you can hide the standard name column (LinkFilename) for ordinary users and display the non-linked column (FileLeafRef). For user with full access you will do the opposite - show the first column and hide the second one.

    Greetings
    Stefan

    ReplyDelete
  11. Alternatively, You can use SPServices and jQuery to Hide SharePoint List Columns based on User Permissions:

    Here is an example: Hide SharePoint List Columns based on User Permissions

    ReplyDelete