SharePoint Security and the Object Model – Part 2

Permissions

This is the second part of a three part post on security object model basics in SharePoint. The other posts are SharePoint Security and the Object Model – Part 1 and SharePoint Security and the Object Model – Part 3.

Defining and Applying Permissions

Authorization in SharePoint is done based on a user’s permission level
for a given context. You can view and manage permission levels by clicking the Permission Levels ribbon button on the People and Groups page.

In the object model the SPRoleDefinition class represents a permission level. The SPRoleDefinition BasePermissions property is a bit mask of SPBasePermission enumeration values. You can bind a role definition to any ISecurableObject via the RoleAssignments property.

The following code shows how to create a permission level that allows editing, but not creation or deletion of items.

SPRoleDefinition def = new SPRoleDefinition();
def.Name = "Edit Not Delete";
def.Description = "Can edit, but can't delete.";


 

def.BasePermissions = SPBasePermissions.ViewListItems |
SPBasePermissions.EditListItems |
SPBasePermissions.Open |
SPBasePermissions.ViewPages;

web.RoleDefinitions.Add(def);

            

Securable Objects and Inheritance

By default, all objects inherit permissions from their parent level. However, as you saw in the previous exercise, you can break inheritance at any level with different permissions for an object and all of its descendants.

Breaking Inheritance

ISecurableObject requires implementation of the BreakRoleInheritance method. SPWeb, SPList, and SPListItem implement ISecurableObject and expose this method.

BreakRoleInheritance accepts a single Boolean argument. BreakRoleInheritance(true) breaks inheritance and copies the role assignments from parent. BreakRoleInheritance(false) breaks role inheritance with no role assignments from parent. A common error is to invoke BreakRoleInheritance(true) when trying to create a unique set of permissions. Because passing true copies the role assignments from the parent, until you modify the role assignments that child object has the same permissions it had before inheritance was broken.

Applying Permissions

Permissions are applied to an ISecurableObject via the RoleAssignments collection. RoleAssignments is a property of type SPRoleAssignmentCollection and contains SPRoleAssignment instances. A role assignment consists of an SPPrincipal (the base class of SPUser and SPGroup) and a collection of RoleDefinitionBindings.

The following code applies a role definition named Create and Edit to a list item and the current SPUser of the web.

void SetPermission(SPListItem item)
{
SPWeb web = item.Web;
item.BreakRoleInheritance(false);

                
SPRoleAssignment assign = new SPRoleAssignment(web.CurrentUser);

assign.RoleDefinitionBindings.Add(
web.RoleDefinitions["Create and Edit"]);

item.RoleAssignments.Add(assign);
}

Author: Doug Ware

SharePoint Security and the Object Model – Part 1

This is the first part of a three part series. The other two parts are SharePoint Security and the Object Model – Part 2 and SharePoint Security and the Object Model – Part 3.

Users and Groups

In the spirit of my post the other day on claims and sign out, I decided to post a complete how-to that also goes along with a talk I do that you may have seen at a user group or SharePoint Saturday. This post is about creating groups and users. The last bit is about how to create a user from a claim.

Creating a Group

The SPGroupCollection class is a collection of SPGroup objects. An SPGroup represents a group in a SharePoint site. Properties of SPGroup include:

  • LoginName
  • Name
  • Description
  • ContainsCurrentUser
  • Owner
  • More…

Two key classes expose properties of type SPGroupCollection. The first is SPWeb. It exposes a Groups collection that contains all of the groups with permissions in the site. It also exposes a second property, SiteGroups, which contains all of the groups in the site collection.

Code to create a group


web.SiteGroups.Add("My Group", web.SiteAdministrators[0], null, "I made this with code!");
web.Update();

                
			

Get the newly created group as an SPGroup and add it to the AssociatedGroups collection so it displays on the quick launch on the Manage Permissions page.

SPGroup group = web.SiteGroups["My Group"];
web.AssociatedGroups.Add(group);
web.Update();

            

Creating a User

The SPUserCollection class is a collection of SPUser objects. SPWeb exposes three properties of type SPUserCollection.

  • AllUsers – Users with explicit membership or who have browsed to the site as authenticated users
  • SiteUsers – All users that belong to the site collection
  • Users – Users with explicit permissions in the site

The names of the first two, AllUsers and SiteUsers, is unfortunate and confusing. AllUsers applies to the site and SiteUsers applies to the entire site collection. Therefore, SiteUsers usually has more items than AllUsers!

The SPGroup.Users property is also of type SPUserCollection. It contains the users with explicit membership in the group.

SPWeb.EnsureUser

The previous section uses the term ‘explicit membership’ a couple of times. It is important to understand the difference between explicit and implicit membership in a site or a group.

Recall that an SPUser is a security principal recognized by the authentication provider and that it can be a user, group, role, or claim. If you add a user directly to a SharePoint group the user has explicit membership in the group. If you add a security principal like an AD group to a SharePoint group, the SPGroup.Users collection will contain a single SPUser instance that represents the AD group. Any users inside the AD group have implicit membership in the SharePoint group. The same is true of claims.

Implicit membership presents a challenge when writing code that deals with users and user information because it is possible and usually certain that there are users with access to a site that SharePoint does not know about via membership in a group. When a user logs in to a site for the first time SharePoint adds an SPUser to the site’s SiteUsers collection along with information about the user, but it is not uncommon to need information about a user who has implicit membership and has never logged in.

SPWeb’s EnsureUser method solves this problem. It ensures that a given name resolves by the authentication system (assuming this functionality is provided) and that it exists in the SiteUsers collection. Attempting to access an SPUser that does not exist throws an exception, and so you should always call EnsureUser whenever it is possible to have a login name and need to access user information for anyone other the current user. Examples include:

  • User name is in item metadata, e.g. Assigned To in a task
  • Code needs email address to send mail given user name
  • Co-worker or workflow assigns task to user who possibly has never accessed the site

The key takeaway is that the best way to get an instance of SPUser is to invoke EnsureUser.

Using EnsureUser with Claims

A claims name is an encoded value that consists of the claim type, issuer, and value, e.g. c:0ȉ.t|stsportalit|staff. The encoded value is what EnsureUser accepts to return an SPUser instance. The SPClaimProviderManager provides methods to encode and decode claims. To encode a claim, first create a new SPClaim. The SPClaim constructor accepts four arguments: claim type, value, value type, and original issuer. For example:

SPClaim claim = new
			

                
SPClaim(@"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
"Fred", ClaimValueTypes.String, "TrustedProvider:TrustedProviderName");

(Replace TrustedProviderName with the actual name of your provider!)

The next step is to decode the claim. First, get the local claim provider manager and then use it to get the encoded name.

string claimName = claimManager.EncodeClaim(claim);


 

Finally, pass the encoded claim to EnsureUser to get an SPUser instance.

SPUser user = SPContext.Current.Web.EnsureUser(claimName);


 

Adding an SPUser to an SPGroup

To add a user to a group, call the AddUser method of SPGroup.

SPContext.Current.Web.SiteGroups["My Group"].AddUser(user);

Author: Doug Ware

Fixing SharePoint, Claims Authentication, and Sign Out

One of my clients has a project that includes claims based authentication with a custom identity provider (IP-STS). This part of the project has been helped by the fact that the documentation gets better each month – sometimes it seems as if the question we have was documented a few days before we needed to ask it!

There is a lot of documentation in the Windows Identity Foundation SDK and in the Identity Developer Training Kit. But in my opinion, the best reference material to date is in the book A Guide to Claims–based Identity and Access Control from Microsoft Patterns and Practices. The companion code for this book is the most complete and well documented example of many scenarios I’ve seen. In particular, the first sample for Single Sign On was most recently helpful to understand what should happen when a user signs out via claims based SSO – something that SharePoint 2010 does incorrectly at the moment! This post is about what should happen and the approach I took to make this work seamlessly with both our custom sites and stock sites that use the SSO.

The Problem

If you use the Sign Out or Sign in as a Different User links in the welcome menu you will most likely be redirected back to the sites home page instead of signing out! This happens based on the following flow.

  1. You sign in to the IP-STS which writes a persisted authentication cookie for the STS on your machine.
  2. You are redirected to the SharePoint site which (by default) writes a persisted authentication cookie for the SharePoint site on your machine.
  3. You click Sign Out whereupon SharePoint deletes the authentication cookie for the SharePoint site and redirects you to the STS (via the SharePoint Sign On page).
  4. The STS uses the authentication cookie it stored on the machine to determine you are already logged in and sends you back to the SharePoint site.

The problem is that SharePoint should redirect you to the identity provider with the query string parameter wa=wsignout1.0. Upon receiving this parameter, the STS will delete its cookie and optionally reply to the relying party via an HTML <img> tag whose src attribute is the relying party with query string parameter wa=wsignoutcleanup1.0. When the RP, in this case SharePoint receives a GET for the URL with this parameter it should delete whatever cookies it must to clean up the session. In the SSO sample that comes from patterns and practices the Sign Out page looks like this:

The check mark is the result of the img tag! <img title="Signout request: http://sp2010:8088/_trust/default.aspx?wa=wsignoutcleanup1.0" src="http://sp2010:8088/_trust/default.aspx?wa=wsignoutcleanup1.0"/> (obvious right?)

A Solution

You can write your own sign out control, but we require the traditional welcome links to work. We need SharePoint sites without our customizations to work as well. One option is to replace the welcome.ascx control, but this is a bad solution – you shouldn’t replace built-in files with your own versions of the same.

The first solution I tried was based on HideCustomAction and CustomAction features to change out the links, but although you can add links to the welcome links menu, the control itself sets the visibility of the stock links in its code-behind and ignores any HideCustomActions! One bug was keeping me from addressing the other!

The actual behavior for Sign Out is in _layouts/signout.aspx. For Sign In as a Different user the page is _layouts/CloseConnection.aspx. Since I could not easily affect the links in the UI I decided to create a simple HttpModule whose salient code is shown below. This code simply redirects to a new page, ClaimsSignOut.aspx if either of the original pages is requested.

void context_BeginRequest(object sender, EventArgs e)
{
var context = (HttpApplication)sender;
string url = context.Request.Url.ToString().ToLowerInvariant();
if(url.Contains(@"/signout.aspx"))
{
context.Response.AddHeader("Location",
url.Replace("/signout.aspx", "/ClaimsSignOut/ClaimsSignOut.aspx"));
context.Response.StatusCode = 301;
context.Response.End();
}
if(url.Contains(@"/closeconnection.aspx"))
{
context.Response.AddHeader("Location",
url.Replace("/closeconnection.aspx", "/ClaimsSignOut/ClaimsSignOut.aspx"));
context.Response.StatusCode = 301;
context.Response.End();
}
}

            

The implementation of ClaimsSignOut.aspx was created by copying SignOut.aspx and using IlSpy to decompile the code behind which I subsequently modified to fix the bug. The fix consists of a simple function to get the IP’s address from the current principal:

private string GetIPUrl()
{
IClaimsPrincipal user = Page.User as IClaimsPrincipal;
if(user == null) return string.Empty;

string ipUrl = string.Empty;
try
{
string providerName = (user.Identity.Name).Split('|')[1];
ipUrl = SPSecurityTokenServiceManager.Local.TrustedLoginProviders[providerName].ProviderUri.AbsoluteUri;
}
catch
{
//TODO:Log it
}
return ipUrl;
}

            

And the following modification to the page’s RemoveCookiesAndRedirect method.

if (iisSettingsWithFallback.UseClaimsAuthentication)
{
string ipUrl = GetIPUrl();
if(ipUrl != string.Empty)
{
string replyUrl = HttpUtility.UrlEncode(SPContext.Current.Site.RootWeb.Url);
string redirect = ipUrl + "?wa=wsignout1.0&wreply=" + replyUrl;

//This next line shouldn't be neccesary if the IP-STS is correct, but clears
//cookies just in case the STS doesn't call back to cleanup the session.
FederatedAuthentication.SessionAuthenticationModule.SignOut();

if (Context.Session != null) Context.Session.Abandon();

Context.Response.Redirect(redirect);
}
else
{
//Original code per reflection – DTW
FederatedAuthentication.SessionAuthenticationModule.SignOut();
int num = 0;

            

Hopefully there will be a hotfix for this at some point and this solution will no longer be needed! In the meantime, you can download the solution from here.

If you see any issues or can make any improvements, please let me know via the comments section!

–Doug

Author: Doug Ware