Temporary Sharing Files in SharePoint

Temporary Sharing is used for adhoc sharing of files in SharePoint with external users.  The scenario would be if you have files on an environment and want to share those with external users such as Marketing Agencies, Legal groups or other external partners you could leverage this external file share to share the file with anyone externally and to have a time expiration on the file access.  The video below shows a demostration of how Temp Sharing in a SharePoint environment might work.

The first part to make the Temp Share application is to enable a custom ribbon control.  You will see on the image below that we have a Temporary Share ribbon icon.  This button is used for single or bulk sharing of documents.  Custom ribbon controls are done through defining a CustomAction and the Javascript for loading the dialog box.

The custom action for the TempSharingButton sets up the notification and dialog framework for when the button is clicked.  When the dialog box launch it reverts back to the Site collection root as the current site.  In our case we are interested in interacting with documents in the currently site and list set.  To keep the context of the current site we need to pass the {SiteUrl} as part of the dialog options.  We also pass a list of the Document IDs to the dialog box which loads the UserShareDialog.aspx control.

The ribbon control loads the External File Sharing dialog up with the selected documents from the list.  The External File Sharing form allows a temp sharing sponsor to specify any external user access by leveraging their partner Email, Facebook account, Gmail account, LiveID, Yahoo, etc.  We enable this trusted authentication through public federation leveraging Azure ACS which we will talk about later in the article.  The sponsor also specifies the amount of time the documents should be shared for.  From this form input a security token is generated using an SHA1 hash.  The token is logged to a SQL database storing temporary shared files and the token is also put on the SharePoint document.

The SharePoint item permissions are changed and the TempShareToken is added, notice below we have the SHA1 hash we generated with a read permission set on the item.  The token is set as a custom claim which our custom claim provider tempshareclaimsprovider will be responsible for filling when I log in with my Facebook account Nathan.Miller@microsoft.com.

Below is code for applying the Temp Share token to the documents. Adding the security token to the document will allow the external user to download or view the shared document.  To add a claim to the document we need to get the provider manager and encode the claim.  When we add the claim to the document it will also add the claim also to the web site with a limited access.  Notice we are breaking item inheritance when we add the token, so this approach should be considered only for specific site collections.

 using (SPWeb web = elevSite.OpenWeb(webUrl))
 {
 SPClaimProviderManager claimMgr = SPClaimProviderManager.Local;
 if (claimMgr != null)
 {
 SPFile file = web.GetFile(docId);
 SPClaim spTokenClaim = Util.GetSharingTokenClaim(sharingToken);
 string claimUserName = claimMgr.EncodeClaim(spTokenClaim);
 // Need to Escalate Privs.
 SPUserInfo userInfo = new SPUserInfo
 {
 LoginName = claimUserName,
 Name = "TempShareToken"
 };
 
 if (file != null)
 {
 web.AllowUnsafeUpdates = true;
 // Add the Claim to the Web.
 SPRoleAssignment roleAssignment = new SPRoleAssignment(userInfo.LoginName, userInfo.Email, userInfo.Name, userInfo.Notes);
 
 // Can't Add Read to the Item.
 SPRoleDefinition roleDefinition = web.RoleDefinitions.GetByType(SPRoleType.Reader);
 roleAssignment.RoleDefinitionBindings.Add(roleDefinition); 
 
 // Add the Claim to the Item.
 SPListItem item = file.Item; 
 
 if (!item.HasUniqueRoleAssignments)
 item.BreakRoleInheritance(true);
 
 item.RoleAssignments.Add(roleAssignment);
 item.Update();
 
 web.AllowUnsafeUpdates = false;
 }
 }

We also want to improve the user experience for sharing content over leveraging just SharePoint permissions.  To do this we added a Sponsors file for the Temp Share file sponsor to manage the files and who he is sharing with.  In this case Brad has two files shared with Nathan.Miller@microsoft.com

 

The Sponsors files are retrieved by calling a Stored Procedure and loading the results property to a web part.  The Unshare link you see on the web part executes an Expire Flag of the document immediately.

 using (SqlCommand cmd = new SqlCommand("dbo.spGet_SharedDocumentsBySponsor")) {
 
 … Code removed for setting up cmd parameters
 
 using (SqlDataReader reader = cmd.ExecuteReader())
 {
 if (reader.HasRows)
 {
 while (reader.Read())
 {
 SharedFile sharedFile = new SharedFile();
 sharedFile.DocumentId = reader.GetString(0);
 sharedFile.Sponsor = sponsorId;
 sharedFile.userShare = reader.GetString(2);
 sharedFile.claim = GetSharingTokenClaim(reader.GetString(4));
 sharedFile.expDate = reader.GetDateTime(5).ToString(); 
 sharedFile.WebUrl = reader.GetString(6); 
 sharedFiles.Add(sharedFile);
 }
 }

Custom Security Provider

We have logged the Shared documents to the database with the Hash code, time expiration and the person we are sharing the file with.  We also changed the security on the item to allow the user we are sharing the item with a user with the email identity of Nathan.Miller@microsoft.com.  Now we need to get the document security tokens loaded as User claims for when the external user logs in.  This is done by using a Custom Security Provider.

Security Providers are farm level features, as a best practice we want to only activate this security provider for a particular web application so in order to enable a claims security provider for only one web app we need to set the provider attribute to not be used by default and then add a Web App Feature which will activate the Security Provider only for the web application where we enable the feature.  To do this we implement a SPFeatureReciever that is scoped at the Web Application level.  In our Web Application feature we are setting up two patterns:

  1. A Web App Policy to deny Write to our Temporary Shared Users.  This protects us from any miss configuration.
  2. We call the toggle TempSharingProvider to enable the provider for our Web Application in the Claims Provider Collection.

 

  public class TempSharingConfigureWebAppFeatureEventReceiver : SPFeatureReceiver {
 // Uncomment the method below to handle the event raised after a feature has been activated.
 
 public override void FeatureActivated(SPFeatureReceiverProperties properties)
 {
 if (properties != null)
 {
 if (properties.Feature.Parent is SPWebApplication)
 {
 SPWebApplication webApp = properties.Feature.Parent as SPWebApplication;
 SPIisSettings settings = webApp.GetIisSettingsWithFallback(SPUrlZone.Internet);
 settings.ClaimsProviders = toggleTempSharingProvider(settings, true);
 
 SPIisSettings defaultZoneSettings = webApp.GetIisSettingsWithFallback(SPUrlZone.Default);
 defaultZoneSettings.ClaimsProviders = toggleTempSharingProvider(defaultZoneSettings, true); 
 
 // Adding the Deny Write to the Web Policy.
 SPPolicyCollection zonePolicies = webApp.ZonePolicies(SPUrlZone.Internet); 
 SPPolicy tokenSharingPolicy = zonePolicies.Add(Util.TokenSharingEnabledClaim.ToEncodedString(), RuntimeConstants.WEBB_APP_POLICY_NAME);
 SPPolicyRole readOnlyPolicy = webApp.PolicyRoles.GetSpecialRole(SPPolicyRoleType.DenyWrite);
 tokenSharingPolicy.PolicyRoleBindings.Add(readOnlyPolicy);
 
 webApp.Update(true);
  }
 }
 }
 
 static List<string> toggleTempSharingProvider(SPIisSettings webAppSettings, bool enable)
 {
 List<string> providers = new List<string>();
 
 foreach (string s in webAppSettings.ClaimsProviders)
 {
 // If the Feature is redeployed and not activated the Provider will get added twice.
 // This is preventing duplicate providers.
 if (!providers.Exists(p => p == s))
 providers.Add(s);
 }
 
 if (enable)
 {
 if (!providers.Exists(p => p == RuntimeConstants.ProviderInternalName))
 providers.Add(RuntimeConstants.ProviderInternalName);
 }
 else
 {
 if (providers.Exists(p => p == RuntimeConstants.ProviderInternalName))
 providers.Remove(RuntimeConstants.ProviderInternalName);
 }
 
 return providers;
 }

User authentication and sharing with our External users is done in our scenario through leveraging public federation and we use Azure ACS in this scenario, so we just need to worry about augmenting the identity claims with our shared tokens when the user logins in.
Our Claims provider is actually fairly simple.  We fill two types of claims.  First, a provider status claim to let us know whether or not to enable the Temp Sharing Ribbon button and the second claim we fill the Sharing Token Claim Type as the key to accessing the individual items. 

  protected override void FillClaimTypes(List<string> claimTypes)
 {
 if (claimTypes == null)
 {
 throw new TempSharingClaimsProviderException(RuntimeConstants.Strings.exp_invalidOrNullClaimTypes);
 }
 claimTypes.Add(RuntimeConstants.SharingTokenProviderStatus);
 claimTypes.Add(RuntimeConstants.SharingTokenClaimType);
 }
 

To fill the sharing token claims for the external user we implement the FillClaimsForEntity in our claim provider.  In this method we call GetSharingTokens for the current user.  This queries our database to retrieve all of the document paths shared for the user and the security Hash token for each document.  We then add the SharingTokenClaimType for each token to the current users claims.

  protected override void FillClaimsForEntity(Uri context, SPClaim entity, List<SPClaim> claims)
 {
 if (claims == null) throw new ArgumentNullException(RuntimeConstants.Strings.exp_invalidOrNullClaimTypes);
 
 //This claim provider is enabled so issue a claim that allows the temp sharing.
 //This will let the user into the web app
 claims.Add(Util.TokenSharingEnabledClaim);
 
 SPClaimProviderManager cpm = SPClaimProviderManager.Local;
 
 //get the current user so we can get to the "real" original issuer
 string userIdForTokenSharing = Util.GetCurrentUserId(entity);
 
 List<SPClaim> documentTokens = Util.GetSharingTokens(userIdForTokenSharing);
 foreach (SPClaim tokenClaim in documentTokens)
 {
 //Issue the sharing token as claims
 claims.Add(tokenClaim);
 }
 }

Our utility method for loading a users SharePoint claims (Util.GetSharingTokens()) with all of the tokens from the documents that are shared with them.

 

  public static List<SPClaim> GetSharingTokens(string userId)
 {
 List<SPClaim> tokens = new List<SPClaim>();
 SPSecurity.RunWithElevatedPrivileges(() =>
 {
 using (SqlConnection conn = new SqlConnection(ConnectionString))
 {
  using (SqlCommand cmd = new SqlCommand("dbo.spGet_TokensForUser"))
 {
  … Removed Parameter code for passing the User ID 
 using (SqlDataReader reader = cmd.ExecuteReader())
 {
 if (reader.HasRows)
 {
  while (reader.Read())
 { 
 tokens.Add(GetSharingTokenClaim(reader.GetString(2))); //Issue the token
 }
 

The external user is now able to log into the system.  As we said before we are using Azure ACS as our Trusted Authentication to SharePoint which will redirects us to the Facebook login in this scenario.

Once we authenticate we are passed to the SharePoint site with our External Users email address as their user ID.

Our internal user shared two documents with nathan.miller@microsoft.com.  We are now able to view these document and download them from our external zone.  Notice we created a web part for our external users to retrieve any of their shared documents.  This works the same way as the Sponsors web part querying the database and showing the documents that are shared through a custom .Net control.

If we take a look at the claims that were filled by our Trusted Login provider (Azure) we see the emailaddress is set as the users identity.  We also see the Facebook AccessToken mapped (though we didn't have to), gmail, yahoo and a partner ADFS are also configured in this demo.  Finally, you can see the claims that we loaded with the provider the tempsharingtoken claim types are the SHA1 hash tokens that we put on the documents.  If the user has the claim token and we have the claim type added to the document with a permission of read, this gives the user access to the documents.

 

TempShare2.0.zip