Monday, September 24, 2012

Copying Permissions From One List To Another List... In Separate Site Collections

My client has a aggregate list that is fed from several lists in many different site collections.  This list is populated by select fields from the feeder lists by an event receiver that fires when a list item's approval status is set to Approved.

The big gotcha here is that permissions and groups are handled at the Site Collection level.  So they do NOT transfer between the site collections.  This is not very intuitive, because...  well...  The permission levels all sound, look, and act the same.   BUT they are all exist in a separate context.  So, you need to do a little bit of code magic to make it happen.

Here we start the code.




private void ConfigurePermissions(SPListItem targetListItem, SPListItem sourceListItem) {
            SPSecurity.RunWithElevatedPrivileges(delegate() {
                using (SPSite sourceSite = new SPSite(sourceListItem.Web.Site.ID)) {
                    using (SPWeb sourceWeb = sourceSite.OpenWeb(sourceListItem.Web.ID)) {
                        sourceWeb.AllowUnsafeUpdates = true;
                        if (sourceListItem.HasUniqueRoleAssignments && sourceListItem.RoleAssignments != targetListItem.RoleAssignments) {
                            SPRoleAssignmentCollection sourceRoles = sourceListItem.RoleAssignments;
                            PropogatePermissions(sourceRoles, targetListItem);
                            targetListItem.Update();
                        } else if (sourceListItem.ParentList.HasUniqueRoleAssignments && sourceListItem.ParentList.RoleAssignments != targetListItem.RoleAssignments) {
                            SPRoleAssignmentCollection sourceRoles = sourceListItem.ParentList.RoleAssignments;
                            PropogatePermissions(sourceRoles, targetListItem);
                            targetListItem.Update();
                        }
                        sourceWeb.AllowUnsafeUpdates = false;
                    }
                }
            });
        }




First, you see that we are wrapping all of the code in a SPSecurity.RunWithElevatedPrivileges delegate.  You need to do this, otherwise the code would run as the user, and that user may not have permissions to execute everything that needs to happen.
Next, you have the standard "using" statements.  Even though the SPListItem object has a reference to the parent SPWeb object, and through that, the parent SPSite object, the SPListItem was instantiated under the user context, and therefore we need to create new objects under the elevated account.  The good news is that we can create the objects using the IDs contained in the SPListItem object, making it very easy and very safe to instantiate the correct site and web.
You'll notice that I have two places that a SPRoleAssignmentCollection could have come from. This is if the list item inherits its permissions or if it has unique permissions.

Now we get in to the meat of the code. I separated the code out in to two methods to help ease readability and debugging. It also made it easier to pass in the correct SPRoleAssignmentCollection, be it from the SPList or the SPListItem

private void PropogatePermissions(SPRoleAssignmentCollection sourceRoles, SPListItem targetListItem) {
            SPSecurity.RunWithElevatedPrivileges(delegate() {
                using (SPSite site = new SPSite(targetListItem.Web.Site.ID)) {
                    using (SPWeb web = site.OpenWeb(targetListItem.Web.ID)) {
                        if (sourceRoles != null) {
                            if (targetListItem.HasUniqueRoleAssignments) {
                                for (int i = 0; i < targetListItem.RoleAssignments.Count; i++) {
                                    targetListItem.RoleAssignments.Remove(i);
                                }
                            } else {
                                targetListItem.BreakRoleInheritance(false);
                            }


I do a check to make sure that the source SPRoleAssignmentCollection has some data.  This is nothing more than a data validation check.  If the object is null there is no reason to go on and face all of the exceptions right?

Next, I check to see if the target SPListItem is inheriting from the parent list, or if it has unique permissions of its own.  The handy dandy HasUniqueRoleAssignments bool property makes this a snap.  If the SPListItem is not inheriting, we simply call the BreakRoleInheritance method passing in false as the property.  This bool property tells the method to copy the existing inherited permissions or to start fresh.  Since we are going to replace the permissions with the permissions from the other list, we pass "false."
If the item already has unique permissions, we need to remove them.
Permissions, weather it be for the list itself or the list item, are kept as SPRoleAssignments in the object's SPRoleAssignmentCollection.  Much like the SPWebCollection object that keep references to all of the SPWebs in a SPSite object, so it goes with the SPRoleAssignmentCollection and SPRoleAssignments.  If the object already has a bunch of SPRoleAssignments in the SPRoleAssignmentCollection, we get rid of them by removing each one.  I do this quickly with a for loop using the SPRoleAssignmentCollection's Count property.

Now that we have a clean SPRoleAssignmentCollection we need to go about populating that collection with the SPRoleAssignments that come from the other list.  Like I mentioned before, permissions are Site Collection based, and, worse yet, SPUser objects are SPWeb based. What does that mean?  If there is no corrisponding SPUser object for your user or group, you can not assign permissions to them.  Even if your list inherits its permissions from the web, and your web is set to allow all Authenticated Users, if the particular user that you want to assign permissions to has not accessed the target web, there is no SPUser object for that user.  As far as the SPWeb is concerned the user doesn't exist.  This can be a real problem.
How do we get around it?  Well, we need to first add the user or group to the web, then create a SPRoleAssingment that will contain that user and map permissions.  That brings us to the next section of code:


foreach (SPRoleAssignment role in sourceRoles) {
                                SPUser newUser;
                                try {
                                    newUser = web.EnsureUser(role.Member.LoginName);
                                    web.EnsureUser(role.Member.LoginName);
                                } catch {
                                    continue;
                                }
                                web.AllowUnsafeUpdates = true;
                                SPRoleAssignment newAssignment = new SPRoleAssignment(newUser.LoginName, newUser.Email, newUser.Name, "");
                                foreach (SPRoleDefinition sourceDef in role.RoleDefinitionBindings) {
                                    string defName = sourceDef.Name;
                                    if (defName == "Limited Access") {
                                        continue;
                                    } else {
                                        try {
                                            SPRoleDefinition newDef = web.RoleDefinitions[defName];
                                            newAssignment.RoleDefinitionBindings.Add(newDef);
                                            targetListItem.RoleAssignments.Add(newAssignment);
                                        } catch {
                                            continue;
                                        }
                                    }
                                }
                            }
                        }
                        web.AllowUnsafeUpdates = false;
                    }
                }
            });
        }


First, I create a loop that will take me through each SPRoleAssingment that the source SPListItem or SPList object has.  Then I create a SPUser.
Now is where things get sticky.  The way I have this code really isn't the best way to solve this, but it worked for me, and I will fix it another time...  Maybe...
What I do first is to fill out the SPUser object by calling the SPWeb.EnsureUser method.  This is a very handy method that will check if the user or group exists in the current context, then add it to the current context if it does not.
If you have any SharePoint groups associated with your permissions you are going to have a hard time.  You will either need to write code to create groups with the same name and permissions in your web, or create these groups ahead of time.
I use Active Directory groups for my permissions, so I don't care at all about SharePoint groups that may be in the SPRoleAssignmentCollection.  I just ignore them, thus the try\catch block that does nothing other than go on to the next permission if there is an exception.  Really, the only exception that can occur is the one that says that the SharePoint group does not exist in the current context.  I don't care about that, because AD groups, as long as they are in AD, can be added directly.

Next we make sure that the Unsafe Updates, like permission changes, are allowed.  One minor gotcha is that if the SPRoleAssingmentCollection.Add method is called, the AllowUnsafeUpdates bool is switched back to false automatically.  We need to confirm that it is True so that we can do our permissions update.

Now we create the new SPRoleAssignment that we will join up this SPUser that we just created with its proper permissions on the SPListItem.  We pass in the properties of the SPUser in to the SPRoleAssignment  object.  Now, it might be tempting to simply pass the SPRoleAssignment.Member object to the new SPRoleAssignment object that we are trying to create.  BUT remember that the SPRoleAssignment.Member is a member of the OTHER site collection.  Not the site collection that houses the target list item.  If we do pass in the SPRoleAssignment.Member from the source site collection, the code will build and it will even execute, BUT the results will NOT be what you are expecting.  In my development envornment I saw the proper object being passed in to the target item's SPRoleAssingmentCollection, but when I checked the collection after the add method was called, I saw that the Member had changed to be the first SPUser that had the same permissions in the web.  Very strange!!
So, to avoid this very real and very dangerous gotcha, we create a brand new SPRoleAssignment and pass in the SPUser properties.

Next I pick apart the actual permission bindings in the form of SPRoleDefinitions.  First, off...  What really annoys me about this particualr section is that Microsoft changed the way they name their collections.  Nearly every collection they have they name so it is very easy to deduce what the colection contains.  The SPSite.Webs is a collection of SPWebs.  The SPList.Items is a collection of SPListItems.  What is the SPRoleAssignment.RoleDefinitionBindings a collection of?  SPRoleDefinitionBindings?  No such object.  It is a collection of SPRoleDefinitions.  Not terribly different from the standard naming convention, but still enough to mess with you and prevent your code from building.
 Anyway, because the same user or group can have multiple permissions assigned to it, you need to create a SPRoleDefinition for each permission and add it to the collection.
Now, a show stopping gotcha appears.  There may be a user, like the System Account or the Search Account, that gets added to the list by the system, and is assigned "Limited Access."  Limited Access is a special permission type, that you can not add a role.  It is a system reserved permission level.  But, it will show up in the RoleDefinitionBindings collection.  It will cause your code to fail if you try to assign this permission level to a user, so, you need to have some code that will handle this possibility.  I simply continue my foreach loop if I encounter it.  You could use some LINQ to filter it out or something else, but, since the foreach loop is pretty performant, I just move on to the next one.

Now we get to the business binding a permission to a user.  We have our user created and added to the web, we have our user added to to the SPRoleAssignment, now we create the SPRoleDefinition to add the permission to the collection.  Because our permissions are going to be named the same, unless you created your own permission levels, then you would need something that would add a similar permission level to your target web, we can just grab the name of the permission from the source web and look it up in the target web.  Like most objects in SharePoint the SPWeb.RoleDefinitions object has an index that you can pass the string name in to.  Here I assign the name of the source definition to a string variable and pass that as the index to the SPWeb.RoleDefinitions object.
Now that I have a RoleDefinition, I add it to the SPRoleAssignment.RoleDefinitionBindings collection.  Then, finally, I add the SPRoleAssignment to the target SPListItem.RoleAssignments collection.
I continue all the way through for all of the objects that are in the source SPListItem.
Cleaning up, I make sure that the target SPWeb has its AllowUnsafeUpdates flag set to false.
Returning to the calling method, I call the SPListItem.Update() method to commit all changes, and finally make sure that the source SPWeb has its AllowUnsafeUpdates flag set to false.

Not horrifically difficult, but there are several gotchas that tripped me up.  I hope you are able to step around them!!!
Here are both of my methods in full form:
private void ConfigurePermissions(SPListItem targetListItem, SPListItem sourceListItem) {
            SPSecurity.RunWithElevatedPrivileges(delegate() {
                using (SPSite sourceSite = new SPSite(sourceListItem.Web.Site.ID)) {
                    using (SPWeb sourceWeb = sourceSite.OpenWeb(sourceListItem.Web.ID)) {
                        sourceWeb.AllowUnsafeUpdates = true;
                        if (sourceListItem.HasUniqueRoleAssignments && sourceListItem.RoleAssignments != targetListItem.RoleAssignments) {
                            SPRoleAssignmentCollection sourceRoles = sourceListItem.RoleAssignments;
                            PropogatePermissions(sourceRoles, targetListItem);
                            targetListItem.Update();
                        } else if (sourceListItem.ParentList.HasUniqueRoleAssignments && sourceListItem.ParentList.RoleAssignments != targetListItem.RoleAssignments) {
                            SPRoleAssignmentCollection sourceRoles = sourceListItem.ParentList.RoleAssignments;
                            PropogatePermissions(sourceRoles, targetListItem);
                            targetListItem.Update();
                        }
                        sourceWeb.AllowUnsafeUpdates = false;
                    }
                }
            });
        }

        private void PropogatePermissions(SPRoleAssignmentCollection sourceRoles, SPListItem targetListItem) {
            SPSecurity.RunWithElevatedPrivileges(delegate() {
                using (SPSite site = new SPSite(targetListItem.Web.Site.ID)) {
                    using (SPWeb web = site.OpenWeb(targetListItem.Web.ID)) {
                        if (sourceRoles != null) {
                            if (targetListItem.HasUniqueRoleAssignments) {
                                for (int i = 0; i < targetListItem.RoleAssignments.Count; i++) {
                                    targetListItem.RoleAssignments.Remove(i);
                                }
                            } else {
                                targetListItem.BreakRoleInheritance(false);
                            }
                            foreach (SPRoleAssignment role in sourceRoles) {
                                SPUser newUser;
                                try {
                                    newUser = web.EnsureUser(role.Member.LoginName);
                                    web.EnsureUser(role.Member.LoginName);
                                } catch {
                                    continue;
                                }
                                web.AllowUnsafeUpdates = true;
                                SPRoleAssignment newAssignment = new SPRoleAssignment(newUser.LoginName, newUser.Email, newUser.Name, "");
                                foreach (SPRoleDefinition sourceDef in role.RoleDefinitionBindings) {
                                    string defName = sourceDef.Name;
                                    if (defName == "Limited Access") {
                                        continue;
                                    } else {
                                        try {
                                            SPRoleDefinition newDef = web.RoleDefinitions[defName];
                                            newAssignment.RoleDefinitionBindings.Add(newDef);
                                            targetListItem.RoleAssignments.Add(newAssignment);
                                        } catch {
                                            continue;
                                        }
                                    }
                                }
                            }
                        }
                        web.AllowUnsafeUpdates = false;
                    }
                }
            });
        }

No comments:

Post a Comment