Writing a Custom Claims Provider for SharePoint 2010 - Part 3: Searching Claims

In the first two parts in this series we've seen how to create a custom provider, do claims augmentation and register the provider, as well as how to add a hierarchy to the people picker. In this post we'll talk about how to implement searching for our claims in the people picker with our custom provider. And this is really where the rubber meets the road. Without having a way to select our custom claim, we can't provision permissions in the site based on who your favorite basketball team is.

First, I want to take a moment to really let that sink in, because this is an important yet not really well understood point. By provisioning permissions based on an attribute of the user, rather than who the user is, it opens ALL sorts of possibilities. It can mean, as it does in this scenario, that I don't care who you are. I don't care how you logged in. I don't care if you're a Windows user or an FBA user. All I care about is who your favorite basketball team is. Here's a classic problem from SharePoint 2007 that still exists in SharePoint 2010 - when I'm at work I log into my SharePoint site using my Windows credentials - Steve Peschka. But when I go home I hit the site via our extranet and I have to use forms based authentication. For our scenario, assume that I am FBA user "user1". What's the problem? Well the issue is that to SharePoint these are two different users. There isn't a way in SharePoint 2007 or SharePoint 2010 to do some kind of user mapping, where we say 'well, the speschka Windows user and user1 FBA user are really the same person'. Guess what? We no longer have to care. Why don't we? Because we aren't assigning permissions on who the person is, we're assigning permissions based on what their favorite basketball team is. The scenario I've implemented is simplified, but you could imagine a piece of corporate metadata that is associated with a person, and the claims provider doing a lookup to some other system to figure out what all the different identities are a person uses - Windows, FBA, PeopleSoft, SAP, CRM, etc. - and being able to map some other identifier or set of claims to that identity. It's then those claims that are used to grant access to resources!

You want single sign on between your different web applications? Again, user1 in webapp1 is not the same as user1 in webapp2. Who cares? As long as I'm using my favorite basketball team claim that person can move seamlessly between those web applications because we are augmenting it each time they authenticate. I don't want to tell you it's perfect because it's not, but it sure is a lot closer to it than previous security models.

Okay, while that sinks in let's start talking about supporting search in the people picker. In order for our custom claims provider to support search we need to add support for the following properties and methods: SupportsSearch, SupportsResolve, FillSearch, FillResolve (the overload with SPClaim as an input parameter), FillSchema and FillEntityTypes.

The properties should be pretty straightforward at this point so let's look at them first:

public override bool SupportsSearch

{

      get

      {

            return true;

      }

}

 

public override bool SupportsResolve

{

      get

      {

            return true;

      }

}

 

FillSearch is probably going to strike you as the most interesting, as it should. So let's look at that code:

protected override void FillSearch(Uri context, string[] entityTypes,

      string searchPattern, string hierarchyNodeID, int maxCount,

      Microsoft.SharePoint.WebControls.SPProviderHierarchyTree searchTree)

{

 

//make sure search is asking for the type of entity we

//return; site collection admin won't for example

if (!EntityTypesContain(entityTypes, SPClaimEntityTypes.FormsRole))

      return;

 

//counter to track what node we're in; will be used to call into

//our helper arrays that were covered in parts 1 and 2 of this series

int teamNode = -1;

 

//NOTE: If we weren't using hierarchies in the picker, then we would

//probably just use a List of PickerEntity instances so that we could

//add them all to the people picker in one call. I've stubbed it out

//here just for illustration purposes so you could see how you

//would do it

//picker entities we'll add if we find something

//List<PickerEntity> matches = new List<PickerEntity>();

 

//node where we'll stick our matches

Microsoft.SharePoint.WebControls.SPProviderHierarchyNode matchNode = null;

 

//look to see if the value that is typed in matches any of our teams

foreach (string team in ourTeams)

{

      //increment team node tracker

      teamNode += 1;

 

      //simple way to do a string comparison to the search criteria

      //this way all a person has to type in to find Blazers is "b"

      if (team.ToLower().StartsWith(searchPattern.ToLower()))

      {

     //we have a match, create a matching entity

            //this is a helper method that I'll explain later

            PickerEntity pe = GetPickerEntity(team);

 

            //if we didn't have a hierarchy we would add it here

            //using the list described above

    //matches.Add(pe);

 

            //add the team node where it should be displayed; make

            //sure we haven't already added a node to the tree

            //for this team's location

            if (!searchTree.HasChild(teamKeys[teamNode]))

            {

                  //create the node so we can show our match in there too

                  matchNode = new

                  SPProviderHierarchyNode(SqlClaims.ProviderInternalName,

                  teamLabels[teamNode],

                  teamKeys[teamNode],

                  true);

 

                  //add it to the tree

             searchTree.AddChild(matchNode);

             }

             else

                  //get the node for this team

                  matchNode = searchTree.Children.Where(theNode =>

                  theNode.HierarchyNodeID == teamKeys[teamNode]).First();

 

            //add the picker entity to our treenode

            matchNode.AddEntity(pe);

      }

}

 

//if we were adding all the matches at once, i.e. we weren't using

//a hierarchy, then we would add the list of matches here

//if (matches.Count > 0)

// searchTree.AddEntities(matches);

}

 

So again, the code is probably pretty self-explanatory. Basically what's happening is the user has typed something in the search box in people picker and click the search button. Our provider is called since our SupportsSearch property returns true. The method that is called is the FillSearch method. In there we look at what the user typed in, which is provided to us with the searchPattern input parameter. We look at all of our team names and check to see if any of them start with the value that was typed in the search box. If it does, we create a new PickerEntity, we either find or create a search tree hierarchy node with the location of that team, and we add our PickerEntity to that node.

Now the code above shows us using a special helper function called GetPickerEntity where were pass in the name of the team we found. Let's look at that code:

private PickerEntity GetPickerEntity(string ClaimValue)

{

 

//use the helper function!!

PickerEntity pe = CreatePickerEntity();

 

//set the claim associated with this match

pe.Claim = CreateClaim(SqlClaimType, ClaimValue, SqlClaimValueType);

 

//set the tooltip that is displayed when you hover over the resolved claim

pe.Description = SqlClaims.ProviderDisplayName + ":" + ClaimValue;

 

//set the text we'll display

pe.DisplayText = ClaimValue;

 

//store it here too in the hashtable **

pe.EntityData[PeopleEditorEntityDataKeys.DisplayName] = ClaimValue;

 

//we'll plug this in as a role type entity

pe.EntityType = SPClaimEntityTypes.FormsRole;

 

//flag the entry as being resolved

pe.IsResolved = true;

 

//this is the first part of the description that shows

//up above the matches, like Role: Forms Auth when

//you do an FBA search and find a matching role

pe.EntityGroupName = "Favorite Team";

 

return pe;

}

 

In a nutshell what's happening here is we're creating a new PickerEntity. As the code comments indicate, the CreatePickerEntity is another helper function that SharePoint provides. I strongly recommend again that you use the helper functions whenever you can. So after we create our PickerEntity we need some way to describe what it "is". In this case, "it" is a claim for favorite basketball team. The way we describe that to the system is to create a new claim, using the same approach we did for our claims augmentation code. This is why we passed in the team name to this helper function, so it could stick the person's favorite team into our favorite team claim. After that we just set a number of properties on the PickerEntity so that it displays and behaves like other claims you see for Windows claims or FBA claims. When we're all done we return the PickerEntity that we created.

We're still not quite done however. If we stopped at this point, we would see the claim in the picker and be able to select it, but when we clicked the OK button to close the people picker dialog, what would happen is that you would see the claim in the type-in control, but it would show up with the red-squiggly underline indicating that it is not resolved. And try as you might, you wouldn't be able to resolve it at that point. What actually happens under the covers is when you click the OK button on the picker dialog to close it, SharePoint tries making another call into your provider, into the FillResolve method. This is an overloaded method; the one that is called into when you close the picker dialog is the one that contains an SPClaim input parameter. So let's take a look at the implementation of that method:

protected override void FillResolve(Uri context, string[] entityTypes,

      SPClaim resolveInput,

      List<Microsoft.SharePoint.WebControls.PickerEntity> resolved)

{

 

//make sure picker is asking for the type of entity we

//return; site collection admin won't for example

if (!EntityTypesContain(entityTypes, SPClaimEntityTypes.FormsRole))

      return;

 

//same sort of code as in search to validate we have a match

foreach (string team in ourTeams)

{

if (team.ToLower() == resolveInput.Value.ToLower())

{

      //we have a match, create a matching entity with helper method

      PickerEntity pe = GetPickerEntity(team);

 

      //add it to the return list of picker entries

      resolved.Add(pe);

}

}

 

So this method is pretty straightforward. You may remember from part 1 of this series that the SPClaim's Value property is going to contain the value for the claim. Since there may have been multiple items selected in the people picker, as we're passed in a claim we enumerate through our team list again to see if the claim value matches one of our teams. If it does, we call our helper method to create a new PickerEntity, and then we add it to the list of PickerEntity instances for SharePoint. Now we see a resolved entry in the type-in control after our selection in the people picker.

There are two other methods you need to implement - they are the FillSchema and FillEntityTypes methods. In FillSchema we are adding the minimal property we want to use in the people picker, which is display name. We do that by adding display name to the schema. In FillEntityTypes we want to return the type of claim(s) that our provider uses. In our case, we have been defining our claim type as SPClaimEntityTypes.FormsRole in the GetPickerEntity method. So I've added that role to the list of entity types:

protected override void

FillSchema(Microsoft.SharePoint.WebControls.SPProviderSchema schema)

{

//add the schema element we need at a minimum in our picker node

schema.AddSchemaElement(new

      SPSchemaElement(PeopleEditorEntityDataKeys.DisplayName,

      "Display Name", SPSchemaElementType.Both));

}

 

protected override void FillEntityTypes(List<string> entityTypes)

{

//return the type of entity claim we are using (only one in this case)

entityTypes.Add(SPClaimEntityTypes.FormsRole);

}

 

To illustrate, here's what people picker looks like after we've done a search for "DVK":

And here's what the type-in control looks like after we've added our claim and clicked the OK button:

So from a code perspective, we are complete, but it's time to ties this back to the discussion at the start of the post...now we can provision permissions based on who your favorite basketball team is. In this case, I've decided that people whose favorite team is DVK Jovenut should be able to contribute content to the site. In this case, I just added this claim to the Members group. Here's how that looks ("Windows Claims" is the name of the site collection; don't let that throw you):

Furthermore, remember what I said about I don't care anymore who you are or how you logged in. By way of example, I have NOT granted ANY Windows user, Windows group, FBA user or FBA role rights anywhere in the site. I've ONLY added my claim for DVK Jovenut to the Members group, and I also added my claim for Blazers to the Visitors group. By way of proof, here's what my list of all groups looks like for the site collection:

Hopefully this will really get you thinking about exactly how powerful claims authentication and claims providers can be. In the final post in this series, I'll show you how to implement support for people typing in a claim name in the type-in control.