Fooling SPS: Download files without having appropriate rights
Last week I did some SharePoint consultancy for a Belgian University (LUC), where they had an interesting issue with SharePoint Portal Server areas. Each research group in the university has it's own WSS site(s) in which they work on documents. Once such a document is finished, the document is published to an SPS area. Ok nothing special you may think, but the tricky part is that only selected people have read access to the WSS site where the actual document is located. Remember that when you publish a document to a SPS area, only a link will appear in the area pointing to the corresponding site. So if a user which doesn't have read rights on the WSS site visits the area, he will see a link to the document. But if he clicks that link, he will get the famous login popup because he cannot access the document! So how do you solve this issue without having to replicate data?
I came up with following solution: impersonating another user to get the document from the WSS site, and passing the document back to the user who requested it in the first place. Of course the process should be transparent for the users. So far the theory, let's take a look at the technical implementation. Because I really like creating web parts, I've put everything inside one web part, built using the SmartPart of course. The web part's first job is to provide an alternative display to display the items that are published to an area. The intresting part here is to figure out which area the web part is located on: // Get current area
SPWeb currentWeb = SPControl.GetContextWeb(this.Context);
Area currentArea = AreaManager.GetArea(PortalContext.Current, currentWeb.ID);
Once you've got a hold of the area instance, you can use the Listings property to loop over all the published items: string html = "";
foreach(AreaListing al in currentArea.Listings)
{
if(al.Status == ListingStatus.Approved)
{
html += string.Format("<a href='{0}'>{1}</a><br>" , this.Request.Url + "?url=" + al.URL ,al.Title);
}
}
Literal1.Text = html;
The code above displays all the items in pretty ugly, but simple, way (a string containing html put in a literal control). I leave it up to your imagination to figure out a nicer way. Notice that the destination of the link is the current page's url, with an url property added to the query string.
The second part of the web part is to do the impersonation magic to get the file which s url is specified in the query string. I've used a WebClient object to accomplish this, you can use it to easily impersonate a specific account and get the binary data of the document: System.Net.WebClient wc = new System.Net.WebClient();
wc.Credentials = new System.Net.NetworkCredential("administrator", "secret");
Byte[] bytes = wc.DownloadData(Request.QueryString["url"]);
Once you get the binary data, you can send it to the Response object using the BinaryWrite method. Before that, you need to add some headers to the Response so the web browser will notice that binary data is sent. The code below will copy all the headers from the WebClient to the Response. At the end the Content-Disposition header is added, so the document will open outside the browser. If you omit this last header, the document will be shown inside the active browser instance. foreach(string headerKey in wc.ResponseHeaders.Keys)
Response.AppendHeader(headerKey, wc.ResponseHeaders[headerKey]);
Response.AppendHeader("Content-Disposition", "attachment;filename=" + fileName);
Response.BinaryWrite(bytes);
If you put both parts together you get following code in the PageLoad event of the Web User Control (remember that I'm using the SmartPart). If you're interested in the complete solution let me know, if there's enough interest, I'll clean up the code some more and publish this web part.
private void Page_Load(object sender, System.EventArgs e)
{
if(Request.QueryString["url"] == "" || Request.QueryString["url"] == null)
{
// Get current area
SPWeb currentWeb = SPControl.GetContextWeb(this.Context);
Area currentArea = AreaManager.GetArea(PortalContext.Current, currentWeb.ID);
string html = "";
foreach(AreaListing al in currentArea.Listings)
{
if(al.Status == ListingStatus.Approved)
{
html += string.Format("<a href='{0}'>{1}</a><br>" , this.Request.Url + "?url=" + al.URL ,al.Title);
}
}
Literal1.Text = html;
}
else
{
Response.Clear();
System.Net.WebClient wc = new System.Net.WebClient();
wc.Credentials = new System.Net.NetworkCredential("administrator", "secret");
Byte[] bytes = wc.DownloadData(Request.QueryString["url"]);
string[] urlParts = Request.QueryString["url"].Split(char.Parse("/"));
string fileName = urlParts[urlParts.Length - 1];
foreach(string headerKey in wc.ResponseHeaders.Keys)
Response.AppendHeader(headerKey, wc.ResponseHeaders[headerKey]);
Response.AppendHeader("Content-Disposition", "attachment;filename=" + fileName);
Response.BinaryWrite(bytes); Response.End();
}
}