Securing SharePoint Content using Profile Attributes

The idea of a profile claim is to augment existing AD groups with additional security based on attributes of the user.   This is useful if an organization wants to secure content for branch employees, stores, clinics, department or employee type.

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. 

An overview of the solution shows that there are 3 main components.

  1. The ProfileClaimsManager is the application page used for saving configuration settings.
  2. The Profile Claim provider is a custom claim provider responsible for loading the users claims and the people picker search logic.
  3. The ProfileClaimsModel.edmx is the entity model used for interacting with the profile database views and the configured claims database.

 

The Profile Claims Manager Page

We use our custom Application page called the Profile Claims Manager to allow administrators to configure profile properties to be used for securing content.  This form will save the configured attribute claims.  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.

 

The claims configure above have the following impact to the portal.  The configured claims in this case Company, Department and EmployeeType will show up in the People Picker.   See below the rendered People Picker after the claims have been configured through the Profile Claims Manager.

Searching on "Strategy" in the people picker will filter and show departments that contain the term Strategy like the "CRM Strategy" and "Strategy Consulting".  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.  Selecting Strategy Consulting will then allow anyone with a Department = Strategy Consulting to access the SharePoint content we secure.

 

 

When a user logs in such as CONTOSO\Brads in this case.  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.  In the middle of the claims list you will see there is a https://schemas.contoso.com/Profile/* these are the claims that were configured through the Profile Claims Manager screen and their values. 

For Brad's case these claims are:

  • Department = "Strategy Consulting"
  • Company = "Contoso"
  • EmployeeType = "Employee"

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.

 

Note:  The Claim Viewer Web Part is just a helper part that is used to dump configured claims.   Here is the code from the claims Viewer. 

  public class ClaimsViewer : WebPart
 {
 protected override void CreateChildControls()
 {
 IClaimsPrincipal claimsPrincipal = Page.User as IClaimsPrincipal;
 IClaimsIdentity claimsIdentity =(IClaimsIdentity)claimsPrincipal.Identity;
 
 GridView gridView = new GridView();
 gridView.ID = "claimsGrid";
 gridView.DataSource = claimsIdentity.Claims;
 gridView.DataBind();
 Controls.Add(gridView);
 }
 }

The Profile Claim Provider

 The Profile Claim provider augments the users claims with profile attribute claims.  There are a couple major parts for the Profile Claims Provider.  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.  In this case we are dynamically filling the claims based on the configuration, so we have to do it a little differently.  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.

Note:  The ClaimsEntities is the database entity model that is used and documented later in this article.

 public static string providerClaimType = "https://schemas.Contoso.com/Profile";
 
protected override void FillClaimTypes(List<string> claimTypes)
{
 if (claimTypes == null)
 throw new ArgumentNullException("claimTypes");

 SPSecurity.RunWithElevatedPrivileges(() =>
 {
 ClaimsEntities claimsDb = new ClaimsEntities();
 
 string claimKey = string.Empty;
 foreach (ClaimProperty claimProperty in claimsDb.ClaimProperties)
 {
 claimKey = String.Format("{0}/{1}", providerClaimType, claimProperty.ProperyName);
 claimTypes.Add(claimKey);
 }
 });
}
 

The Fill Claim For Entity is the main method executed during the User Login process.  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.

 protected override void FillClaimsForEntity(Uri context, SPClaim entity, List<SPClaim> claims)
{ 
 if (entity == null) throw new ArgumentNullException(providerInternalName + ":Invalid Entity");
 if (claims == null) throw new ArgumentNullException(providerInternalName + ":Invalid claims collection");
 
 string userId = entity.Value.Split('|')[1];
 Dictionary<String, String> userProfileProperties = ClaimsHelperClass.GetUserProfileProperties(userId);
 
 foreach (KeyValuePair<String,String> userProfileProperty in userProfileProperties)
 { 
 claims.Add(CreateClaim(string.Format("{0}/{1}",providerClaimType,userProfileProperty.Key),userProfileProperty.Value, claimValueTypeString));
 }
}

Fill Resolve needs to be implemented for the People Picker to resolve a searched or typed in claim.  FillResolve is also called during some system events. 

 protected override void FillResolve(Uri context, string[] entityTypes, string resolveInput, List<PickerEntity> resolved)
{
 // Search a list of FB Groups to use for security. 
 if (!EntityTypesContain(entityTypes, SPClaimEntityTypes.FormsRole)) 
 return;

 // Call our Resolve Claim Value which search the current collection of claims to resolve.
 
 // If we find one add it to the picker.
 List<ProfilePropertyValue> resolveClaims = ClaimsHelperClass.ResolveClaimValue(resolveInput, true);
 
 if (resolveClaims.Count == 1)
 { 
 foreach (ProfilePropertyValue resolvedClaim in resolveClaims)
 resolved.Add(getPickerEntity(resolvedClaim));
 }
}
 

In our implementation the ClaimsHelperClass.ResolveClaimValue is called from each of the FillResolve and the Search.  This allows us to reuse the logic for a wild card search or an exact match search.  Below is the code from the ResolveClaimValue.  It simply queries the entity model and returns a list of claims that match the search criteria.

 public static List<ProfilePropertyValue> ResolveClaimValue(string searchPattern, bool exactMatch)
{
 List<ProfilePropertyValue> claimMatches = new List<ProfilePropertyValue>();

 // If we need to get an exact match on a claim value (such as in FillResolve).
 ClaimsEntities claimsDb = new ClaimsEntities();
 SPSecurity.RunWithElevatedPrivileges(() =>
 { 
 // This can return multiple Properties for the provider, even if Value is an exact match. 
 // Need to do a secondary filter on Property.
 
 if (exactMatch) // If FillResolve on an Exact Match
 {
 var propertyMatches = claimsDb.GetProfileProperty(searchPattern);
 claimMatches = propertyMatches.ToList<ProfilePropertyValue>();
  } 
 else // If using PeoplePicker Search to Find Claims
 {
 var propertyMatches = claimsDb.SearchProfileProperties(searchPattern);
 claimMatches = propertyMatches.ToList<ProfilePropertyValue>();
 } 
});
 return claimMatches; 
}
 

The other key logic area for the Claim provider is the GetPickerEntity.  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.   We configured these claims with an Entity Type of FormsRole.  This treats the claim as a security group augmentation similar to how a Forms security provider would work.

       

 private PickerEntity getPickerEntity(ProfilePropertyValue profileProperty)
{ 
 PickerEntity entity = CreatePickerEntity(); 
 string claimValue = ClaimValue(profileProperty);
 entity.Claim = CreateClaim(string.Format("{0}/{1}",providerClaimType,profileProperty.PropertyName), profileProperty.PropertyVal, claimValueTypeString);
 entity.Description = string.Format("Profile:{0}", claimValue);
 entity.DisplayText = string.Format("{0}", profileProperty.PropertyVal); 
 entity.EntityData[PeopleEditorEntityDataKeys.DisplayName] = claimValue;
 entity.EntityType = SPClaimEntityTypes.FormsRole;
 entity.EntityGroupName = profileProperty.PropertyName;
 entity.IsResolved = true;
 return entity;
}
 
private string ClaimValue(ProfilePropertyValue profileProperty)
{
 return string.Format("{0}={1}", profileProperty.PropertyName,profileProperty.PropertyVal);
} 

 The Profile Claims Entity Model

The solution uses an Entity Model (ProfileClaimsModel.edmx) for filling Claims from the Profiles database and configured profile properties. 

Note: 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.

Database that stores Views for querying Profile database tables and save configured Claim Properties.

User Profile View pulls UserProfile_Full table from the Profile database (in this case [Profile DB] is the profile database.

The User Profile Properties view is used to pull the User, Profile Properties and their values back to fill the Claims.  Notice that a join to the ClaimsProperties table is in the query which will filter to pull back only configured claim values.

Note: SharePoint Claims are an OR not an AND. It isn't possible to add additional security of (Department = HR AND Position = Manager). Though you could extend the profile provider to also issue Audience claims (leveraging Target Audiences) to handle some of the AND attribute scenarios.

ProfileClaims.zip