<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:atom="https://www.w3.org/2005/Atom" xmlns:sy="https://purl.org/rss/1.0/modules/syndication/" xmlns:dc="https://purl.org/dc/elements/1.1/" xmlns:content="https://purl.org/rss/1.0/modules/content/" xmlns:wfw="https://wellformedweb.org/CommentAPI/" xmlns:slash="https://purl.org/rss/1.0/modules/slash/"
  version="2.0">
  <channel>
    <title>Enterprise Dev</title>
    <atom:link
      href="https://docs.microsoft.com/archive/blogs/entdev/feed.xml"
      rel="self"
      type="application/rss+xml" />
    <link>https://docs.microsoft.com/archive/blogs/entdev/feed.xml</link>
    <description>Topics that cover developing using SharePoint, Azure and related technologies.</description>
    <lastBuildDate>Wed, 17 Apr 2019 03:48:50 GMT</lastBuildDate>
    <language>en-US</language>
    <sy:updatePeriod>hourly</sy:updatePeriod>
    <sy:updateFrequency>1</sy:updateFrequency>
    <item>
      <title>Using ADFS and a Custom Claim provider for Authentication and Authorization</title>
      <link>https://docs.microsoft.com/archive/blogs/entdev/using-adfs-and-a-custom-claim-provider-for-authentication-and-authorization</link>
      <pubDate>Tue, 03 Jul 2012 08:15:47 GMT</pubDate>
      <dc:creator><![CDATA[Nathan Miller MSFT]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/entdev/2012/07/03/using-adfs-and-a-custom-claim-provider-for-authentication-and-authorization/</guid>
      <description><![CDATA[This covers the ADFS membership provider we presented at TechEd Orlando and TechEd Europe.&nbsp; I...]]></description>
      <content:encoded><![CDATA[<p>This covers the ADFS membership provider we presented at TechEd Orlando and TechEd Europe.&nbsp; I wanted to post the code and Video, I will follow up with a more detailed walk through.</p>
<p><iframe width="320" height="208" src="https://skydrive.live.com/embed?cid=2DEBB9EB166A05FA&amp;resid=2DEBB9EB166A05FA%212434&amp;authkey=ABHH11j3dQquISc" frameborder="0" scrolling="no"></iframe></p>
<p>&nbsp;</p>
<p>Below is the link to the AD Secure Store Code and Scripts</p>
<p>&nbsp;<iframe width="98" height="120" src="https://skydrive.live.com/embed?cid=2DEBB9EB166A05FA&amp;resid=2DEBB9EB166A05FA%212539&amp;authkey=AKkG6L4RIN6CtHk" frameborder="0" scrolling="no"></iframe></p>
<p>&nbsp;</p>]]></content:encoded>
    </item>
    <item>
      <title>Leveraging Facebook for SharePoint Security</title>
      <link>https://docs.microsoft.com/archive/blogs/entdev/leveraging-facebook-for-sharepoint-security</link>
      <pubDate>Tue, 28 Feb 2012 21:49:27 GMT</pubDate>
      <dc:creator><![CDATA[Nathan Miller MSFT]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/entdev/2012/02/28/leveraging-facebook-for-sharepoint-security/</guid>
      <description><![CDATA[Using Azure Access Control Services we can leverage Facebook, LiveIDs, Yahoo and Google as well as...]]></description>
      <content:encoded><![CDATA[<p>Using Azure Access Control Services we can leverage Facebook, LiveIDs, Yahoo and Google as well as public and private federations to log into SharePoint.&nbsp; The best part is we can do with with requiring code.&nbsp; The video below shows a demostration of using Facebook as a sharePoint login.&nbsp; This demostration also shows how if we do want to extend the security model to secure SharePoint content by Facebook Groups we can do that by leveraging a Custom SharePoint Claim Provider.</p>
<p><iframe style="padding: 0; background-color: #fcfcfc;" title="Preview" height="200" marginheight="0" src="https://skydrive.live.com/embed?cid=2DEBB9EB166A05FA&amp;resid=2DEBB9EB166A05FA%211927&amp;authkey=AAcQnVdZRC5z6iA" frameborder="0" width="320" marginwidth="0" scrolling="no"></iframe></p>]]></content:encoded>
    </item>
    <item>
      <title>Temporary Sharing Files in SharePoint</title>
      <link>https://docs.microsoft.com/archive/blogs/entdev/temporary-sharing-files-in-sharepoint</link>
      <pubDate>Tue, 28 Feb 2012 21:07:00 GMT</pubDate>
      <dc:creator><![CDATA[Nathan Miller MSFT]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/entdev/2012/02/28/temporary-sharing-files-in-sharepoint/</guid>
      <description><![CDATA[Temporary Sharing is used for adhoc sharing of files in SharePoint with external users.&nbsp; The...]]></description>
      <content:encoded><![CDATA[<p>Temporary Sharing is used for adhoc sharing of files in SharePoint with external users.&nbsp; 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.&nbsp; The video below shows a demostration of how Temp Sharing in a SharePoint environment might work.</p>
<p><iframe width="320" height="200" title="Preview" src="https://skydrive.live.com/embed?cid=2DEBB9EB166A05FA&amp;resid=2DEBB9EB166A05FA%211925&amp;authkey=AP6iBvR_1AYj1uQ" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="padding: 0; background-color: #fcfcfc;"></iframe></p>
<p>The first&nbsp;part to make the Temp Share application is to enable a custom ribbon control.&nbsp; You will see on the image below that we have a Temporary Share ribbon icon.&nbsp; This button is used for single or bulk sharing of documents.&nbsp; Custom ribbon controls are done through defining a CustomAction and the Javascript for loading the dialog box.</p>
<p><a href="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/4188.TS1.png" original-url="https://blogs.msdn.com/cfs-file.ashx/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/4188.TS1.png"><img alt="" src="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/4188.TS1.png" original-url="https://blogs.msdn.com/resized-image.ashx/__size/457x244/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/4188.TS1.png" border="0" /></a></p>
<p>The custom action for the TempSharingButton sets up the notification and dialog framework for when the button is clicked.&nbsp; When the dialog box launch it reverts back to the Site collection root as the current site.&nbsp; In our case we are interested in interacting with documents in the currently site and list set.&nbsp; To keep the context of the current site we need to pass the {SiteUrl} as part of the dialog options.&nbsp; We also pass a list of the Document IDs to the dialog box which loads the UserShareDialog.aspx control.</p>
<p><a href="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/4010.TS2.png" original-url="https://blogs.msdn.com/cfs-file.ashx/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/4010.TS2.png"><img alt="" src="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/4010.TS2.png" original-url="https://blogs.msdn.com/resized-image.ashx/__size/550x637/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/4010.TS2.png" border="0" /></a></p>
<p>The ribbon control loads the External File Sharing dialog up with the selected documents from the list.&nbsp; The External File Sharing form allows a temp sharing sponsor to specify any external user&nbsp;access by&nbsp;leveraging their partner Email, Facebook account, Gmail account, LiveID, Yahoo, etc.&nbsp; We enable this trusted authentication through public federation leveraging Azure ACS which we will talk about later in the article.&nbsp; The sponsor also specifies the amount of time the documents should be shared for.&nbsp; From this form input a security token is generated using an SHA1 hash.&nbsp; The token is logged to a SQL database storing temporary shared files and the token is also put on the SharePoint document.</p>
<p><a href="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/3312.TS3.png" original-url="https://blogs.msdn.com/cfs-file.ashx/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/3312.TS3.png"><img alt="" src="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/3312.TS3.png" original-url="https://blogs.msdn.com/resized-image.ashx/__size/550x572/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/3312.TS3.png" border="0" /></a></p>
<p>The SharePoint item permissions&nbsp;are&nbsp;changed and the TempShareToken is added, notice below we have the SHA1 hash we generated with a read permission set on the item.&nbsp; 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 <a href="mailto:Nathan.Miller@microsoft.com">Nathan.Miller@microsoft.com</a>.</p>
<p><img alt="" src="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/3125.TS4.png" original-url="https://blogs.msdn.com/resized-image.ashx/__size/550x103/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/3125.TS4.png" border="0" /></p>
<p>Below is code for applying the Temp Share token to the documents.&nbsp;Adding the security token to the document will allow the external user to download or view the shared document.&nbsp; To add a claim to the document we need to get the provider manager and encode the claim.&nbsp; When we add the claim to the document it will also add the claim also to the web site with a limited access.&nbsp; Notice we are breaking item inheritance when we add the token, so this approach should be considered only for specific site collections.</p>
<pre class="scroll"><code class="csharp">using (SPWeb web = elevSite.OpenWeb(webUrl))<br /> {<br /> SPClaimProviderManager claimMgr = SPClaimProviderManager.Local;<br /> if (claimMgr != null)<br /> {<br /> SPFile file = web.GetFile(docId);<br /> SPClaim spTokenClaim = Util.GetSharingTokenClaim(sharingToken);<br /> string claimUserName = claimMgr.EncodeClaim(spTokenClaim);<br /> // Need to Escalate Privs.<br /> SPUserInfo userInfo = new SPUserInfo<br /> {<br /> LoginName = claimUserName,<br /> Name = "TempShareToken"<br /> };<br /> <br /> if (file != null)<br /> {<br /> web.AllowUnsafeUpdates = true;<br /> // Add the Claim to the Web.<br /> SPRoleAssignment roleAssignment = new SPRoleAssignment(userInfo.LoginName, userInfo.Email, userInfo.Name, userInfo.Notes);<br /> <br /> // Can't Add Read to the Item.<br /> SPRoleDefinition roleDefinition = web.RoleDefinitions.GetByType(SPRoleType.Reader);<br /> roleAssignment.RoleDefinitionBindings.Add(roleDefinition); <br /> <br /> // Add the Claim to the Item.<br /> SPListItem item = file.Item; <br /> <br /> if (!item.HasUniqueRoleAssignments)<br /> item.BreakRoleInheritance(true);<br /> <br /> item.RoleAssignments.Add(roleAssignment);<br /> item.Update();<br /> <br /> web.AllowUnsafeUpdates = false;<br /> }<br /> }</code></pre>
<p>We also want to improve the user experience for sharing content over leveraging just SharePoint permissions.&nbsp; To do this we added a Sponsors file for the Temp Share file sponsor to manage the files and who he is sharing with.&nbsp; In this case Brad has two files shared with Nathan.Miller@microsoft.com</p>
<p>&nbsp;<a href="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/3326.TS5.png" original-url="https://blogs.msdn.com/cfs-file.ashx/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/3326.TS5.png"><img alt="" src="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/3326.TS5.png" original-url="https://blogs.msdn.com/resized-image.ashx/__size/550x191/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/3326.TS5.png" border="0" /></a></p>
<p>The Sponsors files are retrieved by calling a Stored Procedure and loading the results property to a web part.&nbsp; The Unshare link you see on the web part executes an Expire Flag of the document immediately.</p>
<pre class="scroll"><code class="csharp">using (SqlCommand cmd = new SqlCommand("dbo.spGet_SharedDocumentsBySponsor")) {<br /> <br /> &hellip; Code removed for setting up cmd parameters<br /> <br /> using (SqlDataReader reader = cmd.ExecuteReader())<br /> {<br /> if (reader.HasRows)<br /> {<br /> while (reader.Read())<br /> {<br /> SharedFile sharedFile = new SharedFile();<br /> sharedFile.DocumentId = reader.GetString(0);<br /> sharedFile.Sponsor = sponsorId;<br /> sharedFile.userShare = reader.GetString(2);<br /> sharedFile.claim = GetSharingTokenClaim(reader.GetString(4));<br /> sharedFile.expDate = reader.GetDateTime(5).ToString(); <br /> sharedFile.WebUrl = reader.GetString(6); <br /> sharedFiles.Add(sharedFile);<br /> }<br /> }</code></pre>
<h2>Custom Security Provider</h2>
<p>We have logged the Shared documents to the database with the Hash code, time expiration and the person we are sharing the file with.&nbsp; 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 <a href="mailto:Nathan.Miller@microsoft.com">Nathan.Miller@microsoft.com</a>.&nbsp; Now we need to get the document security tokens loaded as User claims for when&nbsp;the external user logs in.&nbsp; This is done by using a Custom Security Provider.</p>
<p>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.&nbsp; To do this we&nbsp;implement a SPFeatureReciever that is scoped at the Web Application level.&nbsp; In our Web Application feature we are setting up two patterns:</p>
<ol>
<li value="1">A Web App Policy to deny Write to our Temporary Shared Users.&nbsp; This protects us from any miss configuration.</li>
<li>We call the toggle TempSharingProvider to enable the provider for our Web Application in the Claims Provider Collection.</li>
</ol>
<p>&nbsp;</p>
<pre class="scroll"><code class="csharp"> public class TempSharingConfigureWebAppFeatureEventReceiver : SPFeatureReceiver {<br /> // Uncomment the method below to handle the event raised after a feature has been activated.<br /> <br /> public override void FeatureActivated(SPFeatureReceiverProperties properties)<br /> {<br /> if (properties != null)<br /> {<br /> if (properties.Feature.Parent is SPWebApplication)<br /> {<br /> SPWebApplication webApp = properties.Feature.Parent as SPWebApplication;<br /> SPIisSettings settings = webApp.GetIisSettingsWithFallback(SPUrlZone.Internet);<br /> settings.ClaimsProviders = toggleTempSharingProvider(settings, true);<br /> <br /> SPIisSettings defaultZoneSettings = webApp.GetIisSettingsWithFallback(SPUrlZone.Default);<br /> defaultZoneSettings.ClaimsProviders = toggleTempSharingProvider(defaultZoneSettings, true); <br /> <br /> // Adding the Deny Write to the Web Policy.<br /> SPPolicyCollection zonePolicies = webApp.ZonePolicies(SPUrlZone.Internet); <br /> SPPolicy tokenSharingPolicy = zonePolicies.Add(Util.TokenSharingEnabledClaim.ToEncodedString(), RuntimeConstants.WEBB_APP_POLICY_NAME);<br /> SPPolicyRole readOnlyPolicy = webApp.PolicyRoles.GetSpecialRole(SPPolicyRoleType.DenyWrite);<br /> tokenSharingPolicy.PolicyRoleBindings.Add(readOnlyPolicy);<br /> <br /> webApp.Update(true);<br /> </code><code class="csharp"> }<br /> }<br /> }<br /> <br /> static List&lt;string&gt; toggleTempSharingProvider(SPIisSettings webAppSettings, bool enable)<br /> {<br /> List&lt;string&gt; providers = new List&lt;string&gt;();<br /> <br /> foreach (string s in webAppSettings.ClaimsProviders)<br /> {<br /> // If the Feature is redeployed and not activated the Provider will get added twice.<br /> // This is preventing duplicate providers.<br /> if (!providers.Exists(p =&gt; p == s))<br /> providers.Add(s);<br /> }<br /> <br /> if (enable)<br /> {<br /> if (!providers.Exists(p =&gt; p == RuntimeConstants.ProviderInternalName))<br /> providers.Add(RuntimeConstants.ProviderInternalName);<br /> }<br /> else<br /> {<br /> if (providers.Exists(p =&gt; p == RuntimeConstants.ProviderInternalName))<br /> providers.Remove(RuntimeConstants.ProviderInternalName);<br /> }<br /> <br /> return providers;<br /> }<br /></code></pre>
<p>User authentication and sharing with our External users is done in our scenario through leveraging public federation and we use Azure ACS&nbsp;in this scenario, so&nbsp;we just need to worry about augmenting the identity claims with our shared tokens when the user logins in.<br />Our Claims provider is actually fairly simple.&nbsp; We fill two types of claims.&nbsp; 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.&nbsp;</p>
<pre class="scroll"><code class="csharp"> protected override void FillClaimTypes(List&lt;string&gt; claimTypes)<br /> {<br /> if (claimTypes == null)<br /> {<br /> throw new TempSharingClaimsProviderException(RuntimeConstants.Strings.exp_invalidOrNullClaimTypes);<br /> }<br /><span style="color: #008000;"> claimTypes.Add(RuntimeConstants.SharingTokenProviderStatus);</span><br /><span style="color: #008000;"> claimTypes.Add(RuntimeConstants.SharingTokenClaimType);</span><br /> }<br /> </code></pre>
<p>To fill the sharing token claims for the external user we implement the FillClaimsForEntity in our claim provider.&nbsp; In this method we call GetSharingTokens for the current user.&nbsp; This queries our database to retrieve all of the document&nbsp;paths&nbsp;shared for&nbsp;the user and the security Hash token for each document.&nbsp; We then add the SharingTokenClaimType for each token to the current users claims.</p>
<pre class="scroll"><code class="csharp"> protected override void FillClaimsForEntity(Uri context, SPClaim entity, List&lt;SPClaim&gt; claims)<br /> {<br /> if (claims == null) throw new ArgumentNullException(RuntimeConstants.Strings.exp_invalidOrNullClaimTypes);<br /> <br /> //This claim provider is enabled so issue a claim that allows the temp sharing.<br /> //This will let the user into the web app<br /> claims.Add(Util.TokenSharingEnabledClaim);<br /> <br /> SPClaimProviderManager cpm = SPClaimProviderManager.Local;<br /> <br /> //get the current user so we can get to the "real" original issuer<br /> string userIdForTokenSharing = Util.GetCurrentUserId(entity);<br /> <br /><span style="color: #008000;"> List&lt;SPClaim&gt; documentTokens = Util.GetSharingTokens(userIdForTokenSharing);</span><br /><span style="color: #008000;"> foreach (SPClaim tokenClaim in documentTokens)</span><br /><span style="color: #008000;"> {</span><br /><span style="color: #008000;"> //Issue the sharing token as claims</span><br /><span style="color: #008000;"> claims.Add(tokenClaim);</span><br /><span style="color: #008000;"> }</span><br /> }<br /></code></pre>
<p style="margin: 0in; font-family: Calibri; font-size: 11pt;">Our utility method for loading a users SharePoint claims (Util.GetSharingTokens()) with all of the tokens from the documents that are shared with them.</p>
<p style="margin: 0in; font-family: Calibri; font-size: 11pt;">&nbsp;</p>
<pre class="scroll"><code class="csharp"> public static List&lt;SPClaim&gt; GetSharingTokens(string userId)<br /> {<br /> List&lt;SPClaim&gt; tokens = new List&lt;SPClaim&gt;();<br /> SPSecurity.RunWithElevatedPrivileges(() =&gt;<br /> {<br /> using (SqlConnection conn = new SqlConnection(ConnectionString))<br /> {<br /> <span style="color: #008000; background-color: #ffffff;"> using (SqlCommand cmd = new SqlCommand("dbo.spGet_TokensForUser"))</span><br /> {<br /> </code><code class="csharp"> &hellip; Removed Parameter code for passing the User ID <br /> using (SqlDataReader reader = cmd.ExecuteReader())<br /> {<br /> if (reader.HasRows)<br /> {<br /> <span style="color: #008000;"> while (reader.Read())</span><br /><span style="color: #008000;"> { </span><br /><span style="color: #008000;"> tokens.Add(GetSharingTokenClaim(reader.GetString(2))); //Issue the token</span><br /><span style="color: #008000;"> }</span><br /> </code></pre>
<p>The external user is now able to log into the system.&nbsp; 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.</p>
<p><a href="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/3757.TS6.png" original-url="https://blogs.msdn.com/cfs-file.ashx/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/3757.TS6.png"><img alt="" src="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/3757.TS6.png" original-url="https://blogs.msdn.com/resized-image.ashx/__size/550x350/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/3757.TS6.png" border="0" /></a></p>
<p style="margin: 0in; font-family: Calibri; font-size: 11pt;">Once we authenticate we are passed to the SharePoint site with our External Users email address as their user ID.</p>
<p><a href="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/4034.TS7.png" original-url="https://blogs.msdn.com/cfs-file.ashx/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/4034.TS7.png"><img alt="" src="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/4034.TS7.png" original-url="https://blogs.msdn.com/resized-image.ashx/__size/317x82/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/4034.TS7.png" border="0" /></a></p>
<p>Our internal user shared two documents with nathan.miller@microsoft.com.&nbsp; We are now able to view these document and download them from our external zone.&nbsp; Notice we created a web part for our external users to retrieve any of their shared documents.&nbsp; 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.</p>
<p><a href="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/8304.TS8.png" original-url="https://blogs.msdn.com/cfs-file.ashx/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/8304.TS8.png"><img alt="" src="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/8304.TS8.png" original-url="https://blogs.msdn.com/resized-image.ashx/__size/550x352/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/8304.TS8.png" border="0" /></a></p>
<p>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.&nbsp; 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.&nbsp; 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.&nbsp; 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.</p>
<p><a href="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/7737.TS9.png" original-url="https://blogs.msdn.com/cfs-file.ashx/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/7737.TS9.png"><img alt="" src="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/7737.TS9.png" original-url="https://blogs.msdn.com/resized-image.ashx/__size/550x249/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/7737.TS9.png" border="0" /></a></p>
<p></p>
<p style="margin: 0in; font-family: Calibri; font-size: 11pt;">&nbsp;</p>
<p><a href="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Components.PostAttachments/00/10/27/41/72/TempShare2.0.zip" original-url="https://blogs.msdn.com/cfs-file.ashx/__key/communityserver-components-postattachments/00-10-27-41-72/TempShare2.0.zip">TempShare2.0.zip</a></p>]]></content:encoded>
    </item>
    <item>
      <title>Securing SharePoint Content using Profile Attributes</title>
      <link>https://docs.microsoft.com/archive/blogs/entdev/securing-sharepoint-content-using-profile-attributes</link>
      <pubDate>Tue, 28 Feb 2012 19:02:00 GMT</pubDate>
      <dc:creator><![CDATA[Nathan Miller MSFT]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/entdev/2012/02/28/securing-sharepoint-content-using-profile-attributes/</guid>
      <description><![CDATA[The idea of a profile claim is to augment existing AD groups with additional security based on...]]></description>
      <content:encoded><![CDATA[<p>The idea of a profile claim is to augment existing AD groups with additional security based on attributes of the user.&nbsp;&nbsp; This is useful if an organization wants to secure content for branch employees, stores, clinics, department or employee type.</p>
<p>The video below will help illustrate the end user experience with this blog entry focused on how the implementation and architecture is implemented for this type of capability.&nbsp;</p>
<p><iframe width="320" height="197" title="Preview" src="https://skydrive.live.com/embed?cid=2DEBB9EB166A05FA&amp;resid=2DEBB9EB166A05FA%211926&amp;authkey=AH-NTtmyFgOVzK8" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="padding: 0; background-color: #fcfcfc;"></iframe></p>
<p>An overview of the solution shows that there are 3 main components.</p>
<ol>
<li value="1">The ProfileClaimsManager is the application page used for saving configuration settings.</li>
<li>The Profile Claim provider is a custom claim provider responsible for loading the users claims and the people picker search logic.</li>
<li>The ProfileClaimsModel.edmx is the entity model used for interacting with the profile database views and the configured claims database.</li>
</ol>
<p>&nbsp;<a href="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/2870.Profile1.png" original-url="https://blogs.msdn.com/cfs-file.ashx/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/2870.Profile1.png"><img alt="" src="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/2870.Profile1.png" original-url="https://blogs.msdn.com/resized-image.ashx/__size/280x306/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/2870.Profile1.png" border="0" /></a></p>
<h2>The Profile Claims Manager Page</h2>
<p>We use our custom&nbsp;Application page called&nbsp;the Profile Claims Manager to allow administrators to configure profile properties to be used for securing content.&nbsp; This&nbsp;form&nbsp;will save the configured attribute&nbsp;claims.&nbsp; Configured claims impact the People Picker in the web application, it also impacts the users during login as it fills the specific user claims based on the users profile values.</p>
<p>&nbsp;<a href="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/4341.Profile2.png" original-url="https://blogs.msdn.com/cfs-file.ashx/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/4341.Profile2.png"><img alt="" src="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/4341.Profile2.png" original-url="https://blogs.msdn.com/resized-image.ashx/__size/550x540/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/4341.Profile2.png" border="0" /></a></p>
<p>The claims configure above have the following impact to the portal.&nbsp; The configured claims in this case Company, Department and EmployeeType will show up in the People Picker.&nbsp;&nbsp; See below the rendered People Picker after the claims have been configured through the Profile Claims Manager.</p>
<p><a href="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/7356.Profile3.png" original-url="https://blogs.msdn.com/cfs-file.ashx/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/7356.Profile3.png"><img alt="" src="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/7356.Profile3.png" original-url="https://blogs.msdn.com/resized-image.ashx/__size/550x560/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/7356.Profile3.png" border="0" /></a></p>
<p style="margin: 0in; font-family: Calibri; font-size: 11pt;">Searching on "Strategy" in the people picker will filter and show departments that contain the term Strategy like the "CRM Strategy" and "Strategy Consulting".&nbsp; How we resolve these search terms is part of our custom claim provider, in this case we decided to only check for a contains in the keyword search.&nbsp; Selecting Strategy Consulting will then allow anyone with a Department = Strategy Consulting to access the SharePoint content we secure.</p>
<p style="margin: 0in; font-family: Calibri; font-size: 11pt;">&nbsp;</p>
<p style="margin: 0in; font-family: Calibri; font-size: 11pt;"><a href="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/1643.Profile4.png" original-url="https://blogs.msdn.com/cfs-file.ashx/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/1643.Profile4.png"><img alt="" src="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/1643.Profile4.png" original-url="https://blogs.msdn.com/resized-image.ashx/__size/550x551/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/1643.Profile4.png" border="0" /></a></p>
<p style="margin: 0in; font-family: Calibri; font-size: 11pt;">&nbsp;</p>
<p>When a user logs in such as CONTOSO\Brads in this case.&nbsp; You will see when we list the claims that get filled during Brads login it shows the windows identities, windows security groups represented by the groupsid ids toward the bottom these are filled in the normal windows/SharePoint claims process.&nbsp; In the middle of the claims list&nbsp;you will see there is a <a href="https://schemas.contoso.com/Profile/">https://schemas.contoso.com/Profile/</a>* these are the claims that were configured through the Profile Claims Manager screen and their values.&nbsp;</p>
<p>For Brad's case these claims are:</p>
<ul>
<li>Department = "Strategy Consulting"</li>
<li>Company = "Contoso"</li>
<li>EmployeeType = "Employee"</li>
</ul>
<p>It's important to note that we are just augmenting the security, normal Kerbros / NTML with AD groups all still works we just add an additional security layer on top for our attributes.</p>
<p style="margin: 0in; font-family: Calibri; font-size: 11pt;"><a href="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/7002.Profile5.png" original-url="https://blogs.msdn.com/cfs-file.ashx/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/7002.Profile5.png"><img alt="" src="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/7002.Profile5.png" original-url="https://blogs.msdn.com/resized-image.ashx/__size/550x400/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/7002.Profile5.png" border="0" /></a></p>
<p style="margin: 0in; font-family: Calibri; font-size: 11pt;">&nbsp;</p>
<p>Note:&nbsp; The Claim Viewer Web Part is just a helper part that is used to dump configured claims.&nbsp;&nbsp;&nbsp;Here is the code from the claims Viewer.&nbsp;</p>
<pre class="scroll"><code class="csharp"> public class ClaimsViewer : WebPart<br /> {<br /> protected override void CreateChildControls()<br /> {<br /> IClaimsPrincipal claimsPrincipal = Page.User as IClaimsPrincipal;<br /> IClaimsIdentity claimsIdentity =(IClaimsIdentity)claimsPrincipal.Identity;<br /> <br /> GridView gridView = new GridView();<br /> gridView.ID = "claimsGrid";<br /> gridView.DataSource = claimsIdentity.Claims;<br /> gridView.DataBind();<br /> Controls.Add(gridView);<br /> }<br /> </code><code class="csharp">}</code></pre>
<h2>The Profile Claim Provider</h2>
<p>&nbsp;The Profile Claim provider augments the users claims with profile attribute claims.&nbsp; There are a couple major parts for the Profile Claims Provider.&nbsp; First, most claim providers register the claims types they fill and usually use a static variable with the claim fill type and the claim value type.&nbsp; In this case we are dynamically filling the claims based on the configuration, so we have to do it a little differently.&nbsp; We need to read the database during the FillClaimTypes and FillClaimValueTypes to retrieve all of the configured claims and add them to the FillClaimTypes registration.</p>
<p>Note:&nbsp; The ClaimsEntities is the database&nbsp;entity model that is used and documented later in this article.</p>
<pre class="scroll"><code class="csharp">public static string providerClaimType = "https://schemas.Contoso.com/Profile";<br /> <br />protected override void FillClaimTypes(List&lt;string&gt; claimTypes)<br />{<br /> if (claimTypes == null)<br /> throw new ArgumentNullException("claimTypes");<br /><br /> SPSecurity.RunWithElevatedPrivileges(() =&gt;<br /> {<br /> ClaimsEntities claimsDb = new ClaimsEntities();<br /> <br /> string claimKey = string.Empty;<br /> foreach (ClaimProperty claimProperty in claimsDb.ClaimProperties)<br /> {<br /> claimKey = String.Format("{0}/{1}", providerClaimType, claimProperty.ProperyName);<br /> claimTypes.Add(claimKey);<br /> </code><code class="csharp">}<br /> </code><code class="csharp">});<br /></code><code class="csharp">}<br /> </code></pre>
<p>The Fill Claim For Entity is the main method executed during the User Login process.&nbsp; Our code simply gets the User Profile Properties from our entity model and adds a new claim for the user based on each configured profile property.</p>
<pre class="scroll"><code class="csharp">protected override void FillClaimsForEntity(Uri context, SPClaim entity, List&lt;SPClaim&gt; claims)<br />{ <br /> if (entity == null) throw new ArgumentNullException(providerInternalName + ":Invalid Entity");<br /> if (claims == null) throw new ArgumentNullException(providerInternalName + ":Invalid claims collection");<br /> <br /> string userId = entity.Value.Split('|')[1];<br /> Dictionary&lt;String, String&gt; userProfileProperties = ClaimsHelperClass.GetUserProfileProperties(userId);<br /> <br /> foreach (KeyValuePair&lt;String,String&gt; userProfileProperty in userProfileProperties)<br /> </code><code class="csharp">{ <br /> claims.Add(CreateClaim(string.Format("{0}/{1}",providerClaimType,userProfileProperty.Key),userProfileProperty.Value, claimValueTypeString));<br /> </code><code class="csharp">}<br /></code><code class="csharp">}</code></pre>
<p>Fill Resolve needs to be implemented for the People Picker to resolve a searched or typed in claim.&nbsp; FillResolve is also called during some system events.&nbsp;</p>
<pre class="scroll"><code class="csharp">protected override void FillResolve(Uri context, string[] entityTypes, string resolveInput, List&lt;PickerEntity&gt; resolved)<br />{<br /> // Search a list of FB Groups to use for security. <br /> if (!EntityTypesContain(entityTypes, SPClaimEntityTypes.FormsRole)) <br /> return;<br /><br /> // Call our Resolve Claim Value which search the current collection of claims to resolve.<br /> <br /> // If we find one add it to the picker.<br /> List&lt;ProfilePropertyValue&gt; resolveClaims = ClaimsHelperClass.ResolveClaimValue(resolveInput, true);<br /> <br /> if (resolveClaims.Count == 1)<br /> { <br /> foreach (ProfilePropertyValue resolvedClaim in resolveClaims)<br /> resolved.Add(getPickerEntity(resolvedClaim));<br /> }<br />}</code><code class="csharp"><br /> </code></pre>
<p>In our implementation the ClaimsHelperClass.ResolveClaimValue is called from each of the FillResolve and the Search.&nbsp; This allows us to reuse the logic for a wild card search or an exact match search.&nbsp; Below is the code from the ResolveClaimValue.&nbsp; It simply queries the entity model and returns a list of claims that match the search criteria.</p>
<pre class="scroll"><code class="csharp">public static List&lt;ProfilePropertyValue&gt; ResolveClaimValue(string searchPattern, bool exactMatch)<br />{<br /> List&lt;ProfilePropertyValue&gt; claimMatches = new List&lt;ProfilePropertyValue&gt;();<br /><br /> // If we need to get an exact match on a claim value (such as in FillResolve).<br /> ClaimsEntities claimsDb = new ClaimsEntities();<br /> SPSecurity.RunWithElevatedPrivileges(() =&gt;<br /> </code><code class="csharp">{ <br /> // This can return multiple Properties for the provider, even if Value is an exact match. <br /> // Need to do a secondary filter on Property.<br /> <br /><span style="background-color: #ffffff;"> if (exactMatch) // If FillResolve on an Exact Match</span><br /> {<br /> var propertyMatches = claimsDb.GetProfileProperty(searchPattern);<br /> claimMatches = propertyMatches.ToList&lt;ProfilePropertyValue&gt;();</code></pre>
<pre class="scroll"><code class="csharp"> } <br /> else // If using PeoplePicker Search to Find Claims<br /> {<br /> var propertyMatches = claimsDb.SearchProfileProperties(searchPattern);<br /> claimMatches = propertyMatches.ToList&lt;ProfilePropertyValue&gt;();<br /> } <br />});<br /></code><code class="csharp"> return claimMatches; <br />}<br /> </code></pre>
<p>The other key logic area for the Claim provider is the GetPickerEntity.&nbsp; This method is responsible for returning the format, display and value of the people picker entity used in each of the Search and Fill Resolve.&nbsp;&nbsp; We configured these claims with an Entity Type of FormsRole.&nbsp; This treats the claim as a security group augmentation similar to how a Forms security provider would work.</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</p>
<pre class="scroll"><code class="csharp">private PickerEntity getPickerEntity(ProfilePropertyValue profileProperty)<br />{ <br /> PickerEntity entity = CreatePickerEntity(); <br /> string claimValue = ClaimValue(profileProperty);<br /> entity.Claim = CreateClaim(string.Format("{0}/{1}",providerClaimType,profileProperty.PropertyName), profileProperty.PropertyVal, claimValueTypeString);<br /> entity.Description = string.Format("Profile:{0}", claimValue);<br /> entity.DisplayText = string.Format("{0}", profileProperty.PropertyVal); <br /> entity.EntityData[PeopleEditorEntityDataKeys.DisplayName] = claimValue;<br /> entity.EntityType = SPClaimEntityTypes.FormsRole;<br /> entity.EntityGroupName = profileProperty.PropertyName;<br /> entity.IsResolved = true;<br /> </code><code class="csharp">return entity;<br />}<br /> <br />private string ClaimValue(ProfilePropertyValue profileProperty)<br />{<br /> return string.Format("{0}={1}", profileProperty.PropertyName,profileProperty.PropertyVal);<br />} </code></pre>
<h2>&nbsp;The Profile Claims Entity Model</h2>
<p>The solution uses an Entity Model (ProfileClaimsModel.edmx) for filling Claims from the Profiles database and configured profile properties.&nbsp;</p>
<p><a href="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/7824.Profile6.png" original-url="https://blogs.msdn.com/cfs-file.ashx/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/7824.Profile6.png"><img alt="" src="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/7824.Profile6.png" original-url="https://blogs.msdn.com/resized-image.ashx/__size/550x542/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/7824.Profile6.png" border="0" /></a></p>
<p><em>Note:&nbsp; The Entity Model connection string is in the solution under App.Config, Application configuration settings need to be deployed to Central Administration, the STS Web Service and the Web App that requires use of the custom claim.</em></p>
<p><a href="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/7433.Profile7.png" original-url="https://blogs.msdn.com/cfs-file.ashx/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/7433.Profile7.png"><img width="858" height="117" style="border: 0px currentColor;" src="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/7433.Profile7.png" original-url="https://blogs.msdn.com/resized-image.ashx/__size/550x0/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/7433.Profile7.png" /></a></p>
<p style="margin: 0in; font-family: Calibri; font-size: 11pt;">Database that stores Views for querying Profile database tables and save configured Claim Properties.</p>
<p><a href="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/8154.Profile8.png" original-url="https://blogs.msdn.com/cfs-file.ashx/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/8154.Profile8.png"><img alt="" src="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/8154.Profile8.png" original-url="https://blogs.msdn.com/resized-image.ashx/__size/222x179/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/8154.Profile8.png" border="0" /></a></p>
<p style="margin: 0in; font-family: Calibri; font-size: 11pt;">User Profile View pulls UserProfile_Full table from the Profile database (in this case [Profile DB] is the profile database.</p>
<p><a href="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/5226.Profile9.png" original-url="https://blogs.msdn.com/cfs-file.ashx/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/5226.Profile9.png"><img width="361" height="62" alt="" src="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/5226.Profile9.png" original-url="https://blogs.msdn.com/resized-image.ashx/__size/334x50/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/5226.Profile9.png" border="0" /></a></p>
<p>The User Profile Properties view is used to pull the User, Profile Properties and their values back to fill the Claims.&nbsp; Notice that a join to the ClaimsProperties table is in the query which will filter to pull back only configured claim values.</p>
<p><a href="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/4251.Profile10.png" original-url="https://blogs.msdn.com/cfs-file.ashx/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/4251.Profile10.png"><img width="733" height="80" style="border: 0px currentColor;" src="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/01/51/20/4251.Profile10.png" original-url="https://blogs.msdn.com/resized-image.ashx/__size/550x80/__key/communityserver-blogs-components-weblogfiles/00-00-01-51-20/4251.Profile10.png" /></a></p>
<p><em>Note:&nbsp; SharePoint Claims are an OR not an AND.&nbsp; It isn't possible to add additional security of (Department = HR AND Position = Manager).&nbsp; Though you could extend the profile provider to also issue Audience claims (leveraging Target Audiences) to handle some of the AND attribute scenarios.</em></p>
<p><a href="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Components.PostAttachments/00/10/27/41/59/ProfileClaims.zip" original-url="https://blogs.msdn.com/cfs-file.ashx/__key/communityserver-components-postattachments/00-10-27-41-59/ProfileClaims.zip">ProfileClaims.zip</a></p>]]></content:encoded>
    </item>
  </channel>
</rss>