SyntaxHighlighter

Thursday, February 13, 2014

Creating Claims Provider for custom IP-STS


I was recently involved in troubleshooting Claims Provider written specifically for custom IP-STS provider. The STS authenticate external user to access public facing SharePoint site collection. The issue was occurring while adding external users to SharePoint group. After selecting User (through people picker control) and adding them to SP group, users were not able to login to the site.

But if I add external users through PowerShell script (see link below for script), users can login and see everything they are suppose to see. After some initial debugging I noticed, the user identity of user added with PowerShell and user added through people picker control were not matching.

//User identity from people picker control (incorrect identity)
c:0ǿ.c|myprovider|testuser2@rtest.com

//User identity using PowerShell script (correct identity)
i:05.t|my provider|testuser2@rtest.com

I googled and found this nice blog link which describes all parts of claims identity. People picker was generating “other claim type” (denoted by “c”) but SharePoint required “identity claim type” (denoted by “i”). And there was one more issue, the trusted issuer (‘myprovider’ and ‘my provider) were not matching either. “my provider” is name of IP-STS provider and “myprovider” is the name of Claims Provider.

With this information, I took another look at claim provider class and could see the issue with code. Here are all the changes which fixed the issue:

First, the claim provider was offering only 1 claim but STS was providing lot more than that. So I updated FillClaimTypes and FillClaimValueTypes.

protected override void FillClaimTypes(List<string> claimTypes)
{
 if (claimTypes == null)
  throw new ArgumentNullException("claimTypes");

 claimTypes.Add(Microsoft.IdentityModel.Claims.ClaimTypes.Role);
 claimTypes.Add(Microsoft.IdentityModel.Claims.ClaimTypes.Upn);
 claimTypes.Add(Microsoft.IdentityModel.Claims.ClaimTypes.Email);
 claimTypes.Add(Microsoft.IdentityModel.Claims.ClaimTypes.Surname);
 claimTypes.Add(Microsoft.IdentityModel.Claims.ClaimTypes.GivenName);
 claimTypes.Add(Microsoft.IdentityModel.Claims.ClaimTypes.System);
}

protected override void FillClaimValueTypes(List<string> claimValueTypes)
{
 if (claimValueTypes == null)
  throw new ArgumentNullException("claimValueTypes");

 claimValueTypes.Add(Microsoft.IdentityModel.Claims.ClaimValueTypes.String);
 claimValueTypes.Add(Microsoft.IdentityModel.Claims.ClaimValueTypes.String);
 claimValueTypes.Add(Microsoft.IdentityModel.Claims.ClaimValueTypes.String);
 claimValueTypes.Add(Microsoft.IdentityModel.Claims.ClaimValueTypes.String);
 claimValueTypes.Add(Microsoft.IdentityModel.Claims.ClaimValueTypes.String);
 claimValueTypes.Add(Microsoft.IdentityModel.Claims.ClaimValueTypes.String);
}

I am using only String types for all claims, but if you are using other value types, make sure the order of ClaimTypes and ClaimValueTypes matches.

Second, to fix trusted issuer mis-match, I replaced base CreateClaim function with this function.
private SPClaim MyCreateClaim(string myClaimType, 
    string myClaimValueType, 
    string myClaimValue)
{
 SPClaim myClaim = new SPClaim(myClaimType, myClaimValue, myClaimValueType,
     SPOriginalIssuers.Format(
        SPOriginalIssuerType.TrustedProvider, “xxx provider”));

    return myClaim;
}

The base CreateClaim function by default creates claim using Name property of SPClaimProvider class as trusted issuer.

Third, return the correct claim type i.e. identity claim. FillSearch method was creating and returning claim of type “FormsRole” which is good for adding additional claims (claim augmentation) but not identity claim. Here is the updated code:
protected override void FillSearch(Uri context, string[] entityTypes, 
    string searchPattern, string hierarchyNodeID, 
    int maxCount, SPProviderHierarchyTree searchTree)
{
    try
    {
        var ldapServer = ConfigurationManager.ConnectionStrings["LDAPConn"];
        var connectionString = ldapServer.ConnectionString;
        var de = new DirectoryEntry(connectionString, 
            LDAPCredentials.UserName, LDAPCredentials.Password,
            AuthenticationTypes.Secure);
        var search = new DirectorySearcher(de);

        search.Filter = 
            string.Format(
                "(&(objectClass=inetorgperson)(|(anr={0})(mail={0}*)))",
                 searchPattern);

        search.SearchScope = SearchScope.Subtree;
        search.PropertiesToLoad.Add("displayName"); 
        search.PropertiesToLoad.Add("mail"); 

        var searchResults = search.FindAll();
        if (searchResults.Count > 0)
        {
          var matches = new List<PickerEntity>();
          foreach (SearchResult profile in searchResults)
          {
            var ups = profile.GetDirectoryEntry();
            var pe = GetPickerEntity(ups.Properties["mail"].Value.ToString(),
               ups.Properties["displayName"].Value.ToString());
            matches.Add(pe);
          }
          searchTree.AddEntities(matches);
    }
    catch (Exception ex)
    {
        LoggingService.LogErrorInULS(ex, TraceSeverity.Unexpected);
    }
}

private PickerEntity GetPickerEntity(string claimValue, string preferredName)
{
    PickerEntity pe = CreatePickerEntity();
    //pe.Claim = CreateClaim(myClaimType, claimValue, myClaimValueType);     
    pe.Claim = MyCreateClaim(myClaimType, myClaimValueType, claimValue);
    pe.Description = string.Format( "{1} [{1}]", “My Provider”, claimValue);
    pe.DisplayText = preferredName ;
    pe.EntityData[PeopleEditorEntityDataKeys.DisplayName] = claimValue;
    pe.EntityType = SPClaimEntityTypes.User; // SPClaimEntityTypes.FormsRole;
    pe.IsResolved = true;
    return pe;
}

The change was simple, change EntityType to SPClaimEntityTypes.User from FormsRole and use custom function to create claim.

Build and deploy package to server.

Here are few links which helped me in resolving the issue:
Claims Walkthrough
Claims Encoding
Writing your own Trusted Identity provider for SP2010 (3) (this link has script to create user through PowerShell)

Hope this helps.

-Javed

Friday, January 31, 2014

Configuring Federated Search for SharePoint 2010


In this post I will discuss how to leverage external search engines which supports OpenSearch protocol and display their results in SharePoint environment.

SharePoint 2010 provides 2 ways to search external data sources. The “content crawling”, in this approach the text context and attributes are indexed and stored by SharePoint search server and the “federated search”, the topic of this post. In this approach SharePoint passes search term and other parameters to external search engine and formats/displays the returned results.
The Federated Search approach provides following benefits :
  1. No need for additional storage, as SharePoint does not index the external content.
  2. We can use external system's native search capabilities.

Federated Result Web Parts:


Federated Results Web Part:

This displays result from specific federated search location. You can only specify one search location per web part.

Top Federated Results Web Part:

You can configure multiple federated search locations with priority and the web part returns result from first federated location which returns search result.
To configure new federated location, follow these steps:
  1. Log in to your SharePoint Central Administration site as a SharePoint farm administrator.
  2. Click "Manage service applications" under the "Application Management" heading.
  3. Click "Search Service Application".
  4. Click "Federated Locations" under the "Queries and Results" heading in the left-hand navigation panel.
  5. Click "New Location".
  6. Fill in the information for the new federated location, and then click 'OK'.
Here are some guidelines on the important fields:
Location Type:
Search Index on this Server: The search happens locally and can be customized to return result from specific scope.

OpenSearch 1.0/1.1: This setting uses external search engine which receives and processes parameter from SharePoint and return structured XML result like RSS or Atom.
Query Template:
The URL template called by SharePoint with search term and other configured tokens. At runtime the tokens are replaced by actual values before calling the URL . The URL should return structured XML like RSS or Atom feed.

http://<server url>/_layouts/CCESearchSF/SearchSF.aspx?q={searchTerms}&start={startItem}&count=3&format=rss

Supported Token Description
{searchTerms} Replaced with search term user types in search box
{startIndex} When fetching results in pages, this token replaced with start index of the first result.
{startPage} When fetching results in pages, this token replaced with page number of set of results to return.
{count} Replaced with number of results to display per page.
More Results Link Template:
The html template contains link to external system search result page.
Triggers:
Always: The search query is always forwarded to federated location.

Prefix: A prefix contains specific term which must be prefixed with search term. eg: "office" prefix with search query like "office New York" will match and "New York" search term is sent to federated location.

Pattern: A regular expression pattern must match the search term to trigger the query to be forwarded to federated location.
Display transformations:
The default transformation provided by SharePoint is good enough in most cases and can be left to default values. But if you prefer to customize the result output, you can replace the default XSLT with your customized XSLT.

Restrictions:

The use of federated location can be un-restricted or restricted to few specific sites or domain.
Authentications:
Anonymous: The federated location does not require authentication.

Common: Every connection uses the same set of credentials to connect to federated location.

Per-User: The credentials of the user who initiated the query is used to connect to federated location. If using Per-user authentication type and the resource is located on different machine, make sure to select "Kerberos" as authentication type.

Hope this helps.

-Javed

Welcome to my blog


This is my second attempt to writing and keeping the blog. My last blog post was around early 2010 and this time I hope to write at least one post every month.

This time I am planning to share all kinds of learning and challenges big or small I faced on the field. I am also planning to convert all my notes, tips/tricks and gotchas I have accumulated on OneNote to blog post.

Lately I have been busy playing with Angular JS, specially building single page application for SharePoint 2013 app model using REST API. I love the power and simplicity it brings to client side development. I will start writing my learning and experience with Angular JS pretty soon.

Happy blogging!

-Javed

Sunday, February 7, 2010

Manually overriding document expiration date


Some time back I implemented interesting functionality which is the topic of this article.
Requirements:
The client wanted to have override feature where users can override expiration date and it should be configurable meaning user can specify number of days/months/years to extend current expiry date. Fortunately we had implemented Custom Expiration formula to calculate expiry date which helps in designing solution for this requirement.
Assumptions:
You are using Custom Expiration formula to calculate expiry date.
Solution:
I took following steps to implement the solution.
  • Create new hidden field "Ignore" of type Number in document library.
  • Create new .aspx page and code behind files. You will find several articles on internet on how to create new application page.

Part of aspx file where all controls are defined.


Part of button click event handler

  • Create feature xml file to provision .aspx file and add "Override Expiration" menu to ECB (Editor Control Block) of list item.
ApplicationPageNavigation.xml (elements xml file)

Feature.xml

  • Modify custom expiration formula code to return already set expiration date when "Ignore" field contains value "1".
  • Build solution (WSP) and deploy to your SharePoint farm.
When user selects "Override Expiration" menu item, he/she is presented with screen where user enters number of days/months/years to extend expiration date. After hitting "Ok" button, the button event code fire and extend current expiration date and redirect user back to list view.

Here are screenshots of final product.

List item with "Override Expiration" ECB menu item and current "Expiration Date"


Screen to capture user inputs (expiry date to extend by 3 days)

List item with updated "Expiration Date"




Attachment:
OverrideExpirationECBMenu.zip

Note the project I have attached is actually taken from proof of concept, so there are no validation and exception handling implemented.

Leave comments if you have any question.

Hope this helps.
-Javed

Friday, February 5, 2010

Validating document approval and sending email notification


Today I was working on a requirement to validate document approval (we are using OOTB document approval feature) based of some external business rules.

Requirement:
If business validation fails, I have to cancel document Approval process and if validation succeeds then I have to generate email notifying team with links to approved document.

Solution:
My first option was to use Workflow using Visual Studio (I have to re-use the functionality for many other document libraries), but soon found out it's not possible to retrieve before and after values inside Workflow and to cancel list item update. Please let me know if you think otherwise. The other option was to use Event Receiver which I followed. I override synchronous ItemUpdating event and again struggled to retrieve after "Approval Status" value. After couple of hours of debugging, I found key named vti_doclibmodstat in AfterProperites collection containing integer values 0, 1 or 2 which is nothing but Approved, Rejected or Pending statuses. So here is the complete code to retrieve before and after "Approval Status" inside event handler:

public override void ItemUpdating(SPItemEventProperties properties)
{
base.ItemUpdating(properties);
SPModerationStatusType beforeStatus = properties.ListItem.ModerationInformation.Status;
SPModerationStatusType afterStatus = 
(SPModerationStatusType)Int32.Parse(properties.AfterProperties["vti_doclibmodstat"].ToString());
if ((beforeStatus != afterStatus) && 
(afterStatus == SPModerationStatusType.Approved))
{
if (Validate(properties) == false)
{
properties.Cancel = true;
properties.ErrorMessage = "";
}
else
{
SendEmail(properties);
}
}
}

And now I can validate document approval process, cancel approval process and send email notifications.

Hope this helps.
-Javed