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;
                    }
                }
            });
        }

Thursday, September 20, 2012

Recruiters

Recruiters...  You guys can really get under my skin.  I get that you have a job to do, and that job entails getting me to leave mine.  I know that you only get paid if I leave my job, and your bonus depends on how much in salary I get paid.  It is a tough job, because the work is on both ends, you need to sell the job to me, and then sell me to the employer.  There is a lot that can go wrong, and I understand that it can be frustrating for you.
HOWEVER, if you are a professional IT recruiter, you need to do a little bit of research in to the actual IT world to know what the hell you are talking about.  There are several things you do that drive me up a wall, and a few that will make me want to never ever use you to nail down an opportunity, no matter how lucrative.
Since this is my blog, and I am a nice guy, Imma give you all some great advice instead of a listing of my pet peeves about recruiters.  In all seriousness, this will help you do better at your job, and will help you gain the trust of your candidates.


  • Know SOMETHING about the skills you are recruiting for.
    • This has got to be my absolute biggest pet peeve about recruiters.  When they call/email me asking about my skill set, and have absolutely no clue about what they are asking about.  I loose all confidence in you representing me when you first ask me if I know .NET, then ask me if I know Visual Studio.  Visual Studio is the programming platform for .NET.  Yes, I know you could use something else to write code in, but NOBODY does.  Don't ask me if I have worked in the Windows Communication Framework, then ask me if I know WCF.  They are the same thing.  Don't listen to my expereince in designing service frameworks using WCF, and ask me if I have ever done SOA development.  Don't ask me if I have developed web parts for SharePoint, then ask me if I know ASP.NET.  SharePoint IS ASP.NET.  ARRRGGGHHH!!!!
    • With just a little bit of reading, and I mean just a little bit, less than 1/2 hour, you could look up those terms on the internet and know, at a very high level, EXACTLY what they are about.  You are a damn professional, do your research!!!!
    • I will forgive a recruiter a lot.  You are not truly IT people, so I don't expect you to know what the MVVM patter is, or where to go when implementing a Managed Metadata Taxonomy.  I do expect you to know the very very very basics so you don't ask any redundant or misguided questions.  If you don't know what my skills are, how can I trust you to find a position that I will fit in to?  If I am hiring you to find me someone, how can I trust you to find me the right person?
  • Never cold call someone to ask them what a technology is.
    • I must get one of these every month.  "Hey, this is Bob from Bob's Tech Recruiting Company.  I saw your resume on line and I wondered if you could tell me what 'SharePoint' is."
      This gives me NO confidence in you representing me.  I will politely direct you to Microsoft's web site for your information after a very short description of what SharePoint is.
      If you have a need for a SharePoint professional, simply come out and say it.  If you don't know what it is, GOOGLE!  Do your research.
  • Look at the candidate's resume first to determine experience.  Make an educated guess as to weather the job that you are looking to fill will fit in to the candidate's experience level.
    • I have written my resume to be very easy to go through.  You can easily see with just a glance that I have many years experience in the stated technologies.  You inspire very little confidence as someone who takes their job seriously if you submit to me entry level jobs when my experience is clearly senior level.  The same is true for an entry level guy getting submitted for senior level positions.
  • Do not balk when you hear salary requirements. 
    • Yes, I might make more than you do.  I also have more experience than you do.  Don't make snide comments, cough, say wow, or make any other comments.  I might be tougher to place than you think.
      I realize the difficulty of finding a job that will keep me in the same pay scale that I am accustomed.  I have been in that position before.     
  • Never simply drop a correspondence if the employer isn't interested in the candidate.  
    • Let me know that the client is no longer interested in me and the reason why.  It is a courtesy call.  It let's me know that we are in this together and that I can trust you to let me know the hard news.  Knowing the reason why helps me become a better candidate in the future, because I will know what I need to do to sharpen my resume, interview skills, or my personal attitude.
      Remember, the better candidate I am, the more likely YOU are able to place me.
  • This is a business deal.  Do not take rejection personally.
    • You are a professional.  You are working on sealing a business transaction.  If I don't like the job or company that you are submitting to me, how is that a personal attack on you?  Don't get upset if I am not ready to leave my current position, or I just don't like what you have put in front of me.  
  • Money is not the only thing that will motivate a candidate to switch jobs.   
    • I have had HUGE numbers thrown at me to work for companies.  I have turned them down.  The reason is that I didn't like the company culture, or some other facet of the job smelled wrong to me.
      I really hate it when recruiters ask over and over again, how much is it going to take to get you to move?  Or this employer is willing to pay more for you.  Blah blah blah.
      Find out what the candidate needs to move.  For me, I will take a significat hit on my salary if more vacation time is offered.  I will sacrifice salary AND vacation time if the opportunity will gain me experience in something that I really want experience in.
      I moved from my last job because my current job offered global deployment experience.  My last job didn't pay me more, but gave me a bunch of vacation time and full benefits.  I don't get benefits now.
      Find out what is important to that candidate and see if the employer will move their direction.

Monday, September 3, 2012

ItemDeleting Event Not Firing

I have written many event receivers.  I have written them for just about every conceivable business use, and for some that are not so conceivable.
I am a MCPD, passing both tests in the 90% range.  I have written these receivers for SPS 2003, MOSS 2007, SharePoint 2010, and even on SharePoint 2013 Beta.

I'm kind of a big deal.

So, why, when I was writing such a mundane application as an event receiver, did one of the events not fire? I overrode the proper method.  I used the correct syntax.  I had no runtime exceptions, and everything built and deployed just fine.  During debugging, as this was a farm deployed solution, I attached to the correct w3wp.exe worker process application (if it would have been a sandboxed solution I would have attached to the user worker process application, SPUCWorkerProcess.exe).  I knew this, because the other events I overrode were firing and doing their jobs just fine.  BUT when it came to the delete events nothing...  What the heck?????????

After looking around and pulling out more than a little bit of hair, out of frustration I created a new solution with just the delete events.  This solution worked just fine.  No muss no fuss.  So, why would the very same code not work in one solution, but not in another???   I did a file comparison of the wsp files to find out what was up.

What happened was this...  When I initially created the project in Visual Studio, I checked the box for just the ItemUpdating event.  Nothing else.  What Visual Studio does with this setting is it adds in the Elements.xml a <Receiver> section in a <Receivers> block.  In that block is an element called <Type>.
What I had forgotten to do with my first solution was to add a <Type> element for my ItemDeleting event.  The Elements.xml file registers the receiver with SharePoint.  If it doesn't have a <Type> section for your events, it doesn't matter how much code you have written in as many overridden methods, the will not fire.
Now, the ItemDeleting event is a separate receiver event, thus it does need its own <Receiver> section.
I just copied my ItemUpdating receiver, and pasted it below.  Then I changed the name and the <Type>.  Everything else can remain the same.

After realizing my mistake, I added the proper <Type> elements to register my deleting events, and BLAMO, they fired just as they should have.
One little trick that may catch many people is that you may have to deactivate then re-activate your feature in order to register the new receiver section, and get the event to fire.  For whatever reason, SharePoint may not load the new XML, even after the IIS Application Pool is recycled.  It can be very annoying, ESPECIALLY if you are trouble shooting other reasons why your event is not firing.

It was a rookie mistake that I made.  Nearly every part of SharePoint development has two parts to it.  The code or assembly part, and the declarative XML part.  The code never works without the XML letting SharePoint know what's up.

One of the good things about software development is that the code will always find away to keep you humble.