Tuesday, February 26, 2013

SPUser and Query-Based Distribution Lists

My current contract is with a company that decided after they installed Exchange 2003 that "if it ain't broke, don't fix it!!!!" So, they haven't updated Exchange in 10 years. It makes life harder in certain ECM and in Records Management situations, but for the most part it really doesn't affect me with my SharePoint work. Until the client wanted to get a task list created from every user in a very particular Distribution List...

First, what is a Query-Based Distribution List? In the Exchange 2003 world it is a security group that contains LDAP query objects, instead of the typical principal entities that normal groups contain. Now, it is a security group in name only. Because it does not contain actual entities, you can't use it to secure anything. You can only use it to email the users that are returned from the query or queries contained within. What makes it tricky is that the membership of the group is dynamic. The user list is created on the fly by the queries every time the group is called. Also, in Exchange 2003, there is no Exchange API that can be used to connect to these groups to resolve the members. Got all that? Good. Here we go!

First things first... In SharePoint if you are given a Security Group or a traditional Distribution List, how do you resolve the membership? Simple!! You need only call the SPUtility.GetPrincipalsInGroup method. That guy will return to you an array of SPPrincipalInfo objects which you can use to create SPUser objects in the manor of your choosing. My preference is to use the SPWeb.EnsureUser method. If the SPUser is not a member of the web or the group EnsureUser will add it and return the resolved SPUser object. If it IS a member it simply returns the SPUser object. It makes no difference if the group is a Distribution List or if the group is a Security Group. This method works for both.
public List<SPUser> GetUsersFromADGroup(string groupName, string groupDisplayName,  System.Collections.Generic.List<SPUser> masterList, SPWeb web) {
bool reachedMaxCount;
SPPrincipalInfo[] principalInfoArray = SPUtility.GetPrincipalsInGroup(web, groupName, int.MaxValue - 1, out reachedMaxCount);
if (principalInfoArray.Count() != 0) {
  foreach (SPPrincipalInfo info in principalInfoArray) {
    if (info.PrincipalType == SPPrincipalType.SecurityGroup || info.PrincipalType == SPPrincipalType.DistributionList) {
     GetUsersFromADGroup(info.LoginName, info.DisplayName,  masterList, web);
   } else {
      try {
       SPUser user = web.EnsureUser(info.LoginName);
        if (!masterList.Any(u => u.Name == user.Name)) {
         masterList.Add(user);
        }
     } catch {
        continue;
     }
   }
  }
  } else {
    GetUsersFromDynamicGroup(groupDisplayName, masterList, web);
  }
    return masterList;
}

A quick bit of code to show how you can get the SPRincipalInfo array using the SPUtility.GetPrincipalsInGroup method, then create SPUsers from that. The try/catch block is there in case there are any orphaned accounts sitting in the groups. These accounts can not be resolved, and will throw exceptions. You can choose to output these to an error list, or simply ignore them as I do here.


BUT, what about the Query-Based Distribution List(QBDL)? Since the QBDL has no actual members, remember the membership of the group is determined by LDAP query, the entities don't actually "belong" to the group, when you call the SPUtility.GetPrincipalsInGroup, you get a SPPrincipalInfo array with no objects. Bummer!

So, where do we go from here? We cannot directly get the group membership. So, have to come at this problem from a different angle. What if we had the LDAP query contained within the AD Query Object? If we had that, we could use .NET's System.DirectoryServices classes to execute it. Great!! So... where is that query held??
The one thing we know for sure about Microsoft Exchange is that they LOVE updating Active Directory Schema. They do it every time there is an update. Fortunately for us, anything that is added to the AD schema, we can very easily pull out and use.
First, you need to add a couple of references. You are going to need to reference System.DirectoryServices and System.DirectoryServices.AccountManagement. Add those guys to your using statements:

using System.DirectoryServices.AccountManagement;
using System.DirectoryServices;

After that we will be constructing the GetUsersFromDynamicGroup method that will be called should the SPPrincipalInfo array return with a count that is equal to 0. You can see the if up in the code, if (principalInfoArray.Count() != 0), and our method GetUsersFromDynamicGroup, being called in the "else" statement.

Now, what do we need to make everything happen. I first need the display name of the group. This is important, because the display name is how the methods in the System.DirectoryServices find the actual group. Why? Because that is how the object is named in LDAP. Note that the SPUtility.GetPrincipalsInGroup uses the login name of the group, NOT the display name. SPUtility.GetPrincipalsInGroup is looking for AD principals, NOT LDAP objects. These two concepts must be kept separate, or this process will not work (LDAP is looking for CN=GroupName,OU=OrgUnitName,DC=DomainName,DC=com, where a principal is looking for a name of DomainName\GroupLogInName).
After I have the display name, I am going to need all of the stuff to make a SPUser so I need the SPWeb object. Since I am returning everything as a List, I want to make sure that I am appending my SPUsers found in the QBDL to whatever else is in the group object, I include the existing List.

The first thing we are going to do in the method is set up a System.DirectoryServices.AccountManagement.PrincipalContext. Why? Well, I need to get a hold of the System.DirectoryServices.DirectoryEntry of the group. From that object, I can read what the members of the group are, and begin to resolve the users. Fortunately, getting the DirectoryEntry is very easy. I create a PrincipalContext using my domain name, then I create a GroupPrincipal using the PrincipalContext and the group display name. Now, it is just a matter of casting the return of the GroupPrincipal.GetUnderlyingObject method as a DirectoryEntry.
PrincipalContext principalContext = new PrincipalContext(ContextType.Domain, "DomainName");
GroupPrincipal groupPrincipal = GroupPrincipal.FindByIdentity(principalContext, IdentityType.Name, groupDisplayName);
DirectoryEntry group = (DirectoryEntry)groupPrincipal.GetUnderlyingObject();

After we have these objects ready to go, we are ready to start the heavy lifting. Now that we have the group as a DirectoryEntry, we can strip out the members of the group, in this case it will be the query objects. Since there really isn't a DirectoryEntry.Members property that will give us a nice DirectoryEntry.MembersCollection, we have to do that on our own. Luckily, we only need to cast the members property as IEnumerable. From there we can create our "foreach" loop and start our work.

object members = group.Invoke("Members", null);
foreach (object member in (IEnumerable)members) {
   //Do work in here
}

Here is where things get a little messy. We now have the Members as generic "objects." We really can't do anything with "objects." C# is a strongly typed language, so we need to transform this "object" in to something that has meaning. So, we need to create a new DirectoryEntry object. We then pass the member object to the DirectoryEntry constructor and, we automagically have a DirectoryEntry object from just a plain old "object." I have to admit here that I don't like using "objects." I like to strongly type everything so there is no confusion at design time or run time as to what objects are and how they can be used... But, because there is no MembersCollection object provided by the DirectoryEntry class, I couldn't figure out away to get the members and put them in a foreach loop. Sure, I could use some other loop, but... I am lazy and I didn't want to. You can make my code suck less and make your own cool loops. I didn't want to worry about it, so I punted and used "object."
Anyway, we have the member as a DirectoryEntry now. This member represents the query object that contains the LDAP query we need to actually resolve the users in the group. So, we can now finally call up the property that stores the LDAP query and execute it!! Yay!!

The property that concerns us is "msExchDynamicDLFilter." Essentially, all we need is that guy's value, and we are off to the next section of our code. Remember that all property objects are Dictionary objects. So you would retrieve the property value the exact same way you would any other dictionary value:
DirectoryEntry.Properties["msExchDynamicDLFilter"].Value.ToString().
Fun, right?

There is one other property that we need to get. Because we want to execute our LDAP query across the entirety of our domain forest, we want to grab the value of the msExchDynamicDLBaseDN property as well. With this guy we will construct the LDAP URI that we will use as our search base.

Now that we have our search base and our LDAP query we are ready to search AD for the members of the Dynamic group. .Net makes this very easy for us, because Microsoft has included a DirectorySearcher class that we can use to search AD. Intuitive, right? Actually it is:
  DirectoryEntry memberEntry = new DirectoryEntry(member);
  string ldapBase = memberEntry.Properties["msExchDynamicDLBaseDN"].Value.ToString();
  ldapBase = string.Format("LDAP://{0}", ldapBase);
  DirectoryEntry adRoot = new DirectoryEntry(ldapBase);
  DirectorySearcher search = new DirectorySearcher(adRoot, memberEntry.Properties["msExchDynamicDLFilter"].Value.ToString());
  SearchResultCollection results = search.FindAll();
 foreach (SearchResult result in results) {
//Create your SPUsers here
}

As you can see, we create the DirectorySearcher object using the the search base (adRoot), and the LDAP query (memberEntry.Properties["msExchDynamicDLFilter"].Value.ToString()). Microsoft provides us with a SearchResultCollection, nice of them, and all we need to do is call the FindAll method to populate it. There is also a FindOne method, if you are only looking for a single item. I'm looking for lots and lots, so I call FindAlll.
As with all of Microsoft's "collection" objects, the SearchResultsCollection inherits IEnumerable, so we can create a foreach loop using it.

Now it is just a matter of getting the property in the SearchResult object that contains the user's login name. After we have that, we need only call SPWeb.EnsureUser(loginName) and we are ready to add our SPUser object to the master List list. We do that the same way that we did it above:

 string loginName = string.Format("TRONOX\\{0}", result.Properties["samaccountname"].Value.ToString());
 try {
    SPUser user = web.EnsureUser(loginName);
    if (!masterList.Any(u => u.Name == user.Name)) {
      masterList.Add(user);
    }
} catch {
    continue;
}


You will notice that I do a little LINQ after calling EnsureUser and before I actually add the SPUser object to the List. I don't want any duplicates, so I check to see if there is a user already in the list with the same Name. If there isn't, I add it to the list. If there is, I ignore the object.

That is all there is to it!! It takes some getting around, but it is possible to get the membership of the QBDL!

Here are both of the methods that I was using in their entirety:

public List<SPUser> GetUsersFromADGroup(string groupName, string groupDisplayName,  System.Collections.Generic.List<SPUser> masterList, SPWeb web) {
 bool reachedMaxCount;
 SPPrincipalInfo[] principalInfoArray = SPUtility.GetPrincipalsInGroup(web, groupName, int.MaxValue - 1, out reachedMaxCount);
 if (principalInfoArray.Count() != 0) {
  foreach (SPPrincipalInfo info in principalInfoArray) {
    if (info.PrincipalType == SPPrincipalType.SecurityGroup || info.PrincipalType == SPPrincipalType.DistributionList) {
     GetUsersFromADGroup(info.LoginName, info.DisplayName,  masterList, web);
   } else {
      try {
       SPUser user = web.EnsureUser(info.LoginName);
        if (!masterList.Any(u => u.Name == user.Name)) {
         masterList.Add(user);
        }
     } catch {
        continue;
     }
   }
  }
  } else {
    GetUsersFromDynamicGroup(groupDisplayName, masterList, web);
  }
    return masterList;
}


public List<SPUser> GetUsersFromDynamicGroup(string groupDisplayName, List<SPUser> masterList, SPWeb web) {
 PrincipalContext principalContext = new PrincipalContext(ContextType.Domain, "TRONOX");
 GroupPrincipal groupPrincipal = GroupPrincipal.FindByIdentity(principalContext, IdentityType.Name, groupDisplayName);
 DirectoryEntry group = (DirectoryEntry)groupPrincipal.GetUnderlyingObject();
 object members = group.Invoke("Members", null);
  foreach (object member in (IEnumerable)members) {
    DirectoryEntry memberEntry = new DirectoryEntry(member);
    string ldapBase = memberEntry.Properties["msExchDynamicDLBaseDN"].Value.ToString();
    ldapBase = string.Format("LDAP://{0}", ldapBase);
    DirectoryEntry adRoot = new DirectoryEntry(ldapBase);
    DirectorySearcher search = new DirectorySearcher(adRoot, memberEntry.Properties["msExchDynamicDLFilter"].Value.ToString());
    SearchResultCollection results = search.FindAll();
    foreach (SearchResult result in results) {
       string loginName = string.Format("TRONOX\\{0}", result.Properties["samaccountname"].Value.ToString());
       try {
          SPUser user = web.EnsureUser(loginName);
             if (!masterList.Any(u => u.Name == user.Name)) {
                masterList.Add(user);
             }
       } catch {
           continue;
       }
    }
  }
 return masterList;
}

New Button on DispForm.aspx, Deploying With a Solution

I was asked by my client to create a button on the "View Task" form ribbon that the user could click and automatically complete the task. Sounds easy right? Well, if you are using SharePoint designer it is. SharePoint Designer has a straight forward and easy way to deploy new ribbon buttons, pretty much anywhere you want. They even have a nice step by step instruction manual on how to go about doing it:
Create a custom list form using SharePoint Designer

But what if you want to deploy that button to many different farms? From your development farm to your production farm as part of a feature deployment, perhaps? What do you do? Well, it gets a little more complex now. You need to find the correct ribbon to add the the button to. But, SharePoint uses the DispForm.aspx for... well... all of its forms, so what to do?

I am trying to make it harder than it seems. It is actually a piece of cake if you have done any ribbon manipulation at all. And if you haven't, well... maybe this would be an easy one to start on.

First, you need to know about how to make a generic button on a typical list page. Chris O'Brien does an EXCELLENT job of describing how to do that. Go to his blog for instructions.

Got a basic idea? Good. Now that you know how to make ribbon tabs and buttons, you basically know how to create and place buttons and tabs on virtually any page that SharePoint has. You just need to know the LOCATION!! The location is that pesky little property in the CommandUIDefinition. Most of the time you will be putting your button with the other buttons SharePoint has so your property will look very similar to this: Location="Ribbon.Tabs._children

But, let's circle back to the topic of this post. We don't want a new button on the list page we want it on the DISPLAY page, or DispForm.aspx. Turns out there is a location for that. Just follow all of Chris' instructions on how to add a button or tab, but change your CommandUIDefinition Location property to Ribbon.ListForm.Display.Manage.Controls._children

This is the ribbon location for what you see on the DispForm.aspx page. You can package up your button in a feature and deploy to where ever you would like it. No need to change the form page in every farm!

Tuesday, February 5, 2013

The User Task List Web Part vs The Content Query Web Part

I have a requirement for a task roll up web part. I have an Event Receiver that spawns a new task list for every list item in another list. The receiver looks in to a People Picker Field (SPFieldUserMulti)and creates a new task for all of the users in the field.

We wanted a web part that would only show the user's active tasks to place on the site home page so that the user could seamlessly find the tasks assigned to them, without having to transverse a bunch of task lists. So how to do this? There are a couple of different ways. If you are looking for a quick Out Of the Box (OOB) way, you need only drop the User Task List Web Part (UTLWP), from the Social Collaboration Web Part Group, on your page and you are done!! The User Task List will look in to the site and show all of the tasks, that are not marked as complete, in a list. Very easy and very quick.

The other way to do things is almost as easy, you drop a Content Query Web Part (CQWP), from the Content Roll Up Web Part Group, on your page. You set the query to list Type Tasks, and then configure the Additional Filters to look at the Assigned To site column with a setting of equal to [Me]. The next filter should be the Task Status site column, and set the configuration to not equal "Completed."

Now we have two web parts that will show the tasks assigned to the user throughout the site, all in a matter of just a few moments! Very cool!

So... What if we want more? What if we just want the tasks assigned to the user, but we want them to be current. In other words, we want the task to be assigned to the user, have a task status set to anything but Completed, and the due date must be greater than today.
Now the ease of the User Task List Web Part falls away. For some reason, the makers of the web part didn't add a way to easily change the configuration. It is not possible to change the internal query of the UTLWP to add the extra filter of Due Date. Bummer.

However, the CQWP can be very quickly configured to make this extra change happen, we simply add another filter to our query that sets the Due Date to be greater than or equal to [Today] in the Additional Filters section. Blamo, that easy.

Now, what if we have task lists in other sites within the Site Collection? What if we want to aggregate these tasks to the page in the root site?

The CQWP base query uses the SPSiteDataQuery object to search through all sites in the site collection for those items that meet the search criteria. So, it AUTOMATICALLY will aggregate the sites and display the items that meed the query criteria. Dead easy, right?

The UTLWP will only show tasks in the current site. Big bummer. BUT, you can change the base query from the standard site query to a SPSiteDataQuery by doing a few minor steps. First you drop the UTLWP on to your page. Then click the down arrow on the upper right hand of the web part. Select "Export." This will download the web part on to your local computer as a .dwp file. This is just an XML file of the web part's settings.
Open the DWP file in a text editor, preferably one that will color code and format the text as XML. This is not a requirement, but it really does make it easier to read.
At the bottom of the file just above the closing </WebPart> tag put in this tag:
<QuerySiteCollection xmlns="http://schemas.microsoft.com/WebPart/v2/Aggregation">true</QuerySiteCollection>
Save the file and go back to your SharePoint page. In the Web Part selector ribbon, there is a link to upload a web part. Simply click on this and upload the dwp file you just manipulated. Then just drop it on your page.
BINGO!! You have a UTLWP that will show all the uncompleted tasks for the user in all sites in the site collection.

So, in the battle between these two very useful web parts, who wins? Of course it all depends on your business needs. My particular business needs point me towards the more flexible CQWP, because I need to show the tasks for the user that are not complete, and not overdue. It also allows me to add another CQWP on the page to show the user the tasks that are not complete and ARE overdue. The UTLWP, while very useful, does not allow me to do that.

Wednesday, December 12, 2012

SharePoint 2010 - Alternate Access Mappings, Fully Qualified Domain Names, and Access Denied Errors

Back in the good old days of SharePoint Portal Server 2003 and Microsoft Office SharePoint Server 2007, Alternate Access Mappings (AAM) were easy. All you did was add a binding to your IIS web site, add a mapping in Central Administration and you were good to go. Not so in SharePoint 2010. The new Claims Based Authentication structure changes the ballgame considerably. Now, Zones are much more important, and because everything is a claim now, the ability to change authentication methods in a different zone is much more complex. From a SharePoint Architecture point of view. From an Administration point of view, you just need to know a couple of gotchas to avoid getting tripped up.

Let's start with a fairly common situation. Your company uses SharePoint as its intranet. You only have one domain, so you can keep things simple on the DNS host names. You call your site SP2010. Then, for what ever reason, you need to make your site resolve to the fully qualified domain name (FQDN), SP2010.MyCompany.com. Easy enough, right? First you attempt to hit the site with the FQDN. You get the IIS 7 Welcome screen, or a 404 error.
You check the Application log on your SharePoint server and find an error that says:
A request was made for a URL http://SP2010.MyCompany.com which has not been configured in Alternate Access Mappings. Some links may point to the Alternate Access URL for the default zone http://SP2010. Review the Alternate Access mappings for this Web application at http://CENTRALADMINISTRATIONADDRESS/_admin/AlternateUrlCollections.aspx and consider adding http://SP2010.MyCompany.com as a Public Alternate Access URL if it will be used frequently.
Ok... Awesome.

You go to Central Administration, click on System Settings, and then Configure alternate access mappings, and are greeted with the AAM page.

Things are a little confusing here. To be honest, this page is just plan awful, especially if you have never made an AAM in SharePont 2010. After you know a little about what is going on, it makes more sense, but at first look... Terrible.
There are several buttons across the top allowing you to edit Public URLs and adding internal URLs and Mapping to External Resources. Don't worry about these for now.

The first thing we need to do is to change the view so that we only see the URLs for the Web Application that we are working with. On the right side of the screen, use the drop box to change the view from "Show All" to "SP2010." You click on the drop box and click the button that appears. A selection screen shows up, and you can pick the AAM collection that you will be working with. AAM collections are named according to the Web Application that contains them, so we click on SharePoint - 80, the name of our Web Application. Straightforward right? Ha! At least they give us the default URL so that we know a little bit about what is going on.

Now the AAM screen is back with just the URLs we are worried about.

What to do? First things first... We click on Edit Public URLs. (Disclaimer: After I get done with all of this, you are going to ask why I do my change this way, an not just add an Internal URL. You could. I am configuring this way to demonstrate the differences in zones and use the most confusing pages so that the explanation is clear, and can be used in multiple situations. It also looks a lot cleaner to have your FQDN as the public URL. ;-P)
If the AAM page was bad this page is much much worse.

What now??? It looks like we could just add a new URL here and everything will be cool right? No. Microsoft should have had some sort of validation or something here, because you can't just add a URL for a zone. You have to create the zone first. And... zones are not created in this area. If you want to create a new zone, you have to extend your Web Application in to a new IIS web site. If you add a URL in to one of the zone boxes provided, and try to hit the site, SharePoint and IIS don't know what to do with the traffic. If you are using Host Headers, you are likely to get the IIS 7 Welcome screen. If you are using ports or individual IP addresses, you are likely to get 404 or access denied errors. All this page does is tell SharePoint that traffic for a specific zone will come in over a specific URL. Nothing else.
Right now, we only have one zone defined for this Web Application, the Default zone. So, we can adjust that one. Change the default URL to have the FQDN address, and click Save. Remember that NO validation, other than a check to see if URL that you have saved matches another URL in any collection, is done on these URLs. SharePoint doesn't add anything to IIS in terms of bindings. No host headers are added. Your URL could be completely bogus, and SharePoint won't care.


Now we can do something that is actually pretty cool. In SharePoint 2010, you can have a many to one relationship with internal URLs to a single public URL. So, really you can define any URL you want for your SharePoint sites. As long as you add them to IIS and DNS. More on that later. For now, click on Add Internal URLs.

Here we get a new and somewhat less confusing page, but Microsoft gives us enough ambiguity and lack of validation to hang ourselves with.


Enter in any URL you wish. In our case we want the short URL of http://SP2010. The area that can get us in to trouble, again, is the Zone drop box. Remember we only have one zone defined, the Default zone. So even though you can change the zone, if you haven't extended your web application to create that zone, DON'T CHANGE it from Default. Click save and you go back to the AAM screen, provided that the URL you typed isn't in use by another Web Application.


You now see that we have definitions for both URLs, associated with the same Public URL. This is all we need to do to define our AAM. There is a bit of housekeeping you need to do though...

What we have just done is a SharePoint specific activity. It changes how SharePoint routs its own traffic and how SharePoint handles its own authentication. What we did does NOT change how DNS or IIS works. Those are separate process. If your URLs are not registered in DNS you will need to contact your DNS administrator to add them for you. If you are on your own for DNS work, and you don't know how to add a Host record (also called an "A" record), go here: DNS How To from Technet

For adding a host header in IIS go here: How to add a Host Header binding from Technet

If you have DNS, IIS, and AAM all configured correctly you now have your site set up to use both the short host name and FQDN for SharePoint. You also know some more about SharePoint zones, and how you can very quickly, and easily, add a URL to a SharePoint site.

Saturday, November 10, 2012

How Obamacare Will Help You

By YOU I mean if you have a few years of experience in the IT world. If you are brand new to the game... Sorry, but you are going to have a hell of a time just finding a job. What we will see now that Obamacare is a certainty, is that many companies are going to be laying off people. Companies, especially small business, especially will be working hard to shed workers off their roles to reduce their costs. This is bad. So, how can I work this to my advantage? Well, companies will still need IT work to be done. If they intend to stay in business, they will need MORE IT to automate processes that normally would be filled by workers. This means things like collaboration solutions, workflows, and administrative scripting will become very hot. Since business can't hire FTEs to do this work, who will do it? Contractors, consultants, and other temp workers can fill the personnel void without having to hire them full time. BUT, since most consulting agencies are, in fact, small business, it will be difficult to find a position at an actual consulting company. So how does this help me? Enter independent consulting. With a little leg and research work, you can set yourself up with your own company, with one employee, you. If you do the leg work before 2013, you can get ahead of the game and be ready if you are laid off. Companies will be looking for consultants, and you will be set up and ready to go. Now, working as an independent consultant means that you will have to pay for your own... everything. Insurance, taxes, social security, blah blah blah. You will have to figure out how much to charge per hour so that you can turn a profit. In this case profit means personal income, so remember that you are negotiating for bread on the table. HOWEVER, it is important that you keep your income below $250,000 after that amount you will really take it in the shorts from the tax man. Good luck in the coming months. They won't be easy for any of us. IT is seen as overhead for many business, so you can guarantee if cuts are to be made it will be with us.

Monday, October 1, 2012

Windows 2012 / 8 HID Service and Fancy Keyboards

After a catastrophic crash of my Windows 2008 R2 server, read I screwed up a BIOS update *sigh*,  I migrated my OS to Windows 2012.  The install of the Desktop experience was fairly straight forward, very similar to the procedure for Windows 2008, the only gotcha involved was finding the feature (it's under the User Interfaces and Infrastructure heading).

The big gotcha so far has been with my fancy keyboard. I have a Microsoft Natural Ergonomic Keyboard.  It is awesome.  Not only is it nice on the wrists, but it has handy dandy hot buttons that control volume, mute, launches the calculator, and other programmable buttons.  They are really handy.  BUT they didn't work after I migrated.

Everything I checked seemed to be working well, correct drivers were installed, nothing funny going on in the  event logs, the buttons just didn't work.

I then looked a bit harder at what showed up in the Device Manager:

HID keyboard...  Hummm....  There was a service in Windows 2008 that was HID.  In Windows 2012 the service is, Human Interface Device Access service.


As soon as I started that guy, all of the little buttons started to work.  An easy fix, but a necessary one if you have anything on Windows 2012 that has extras, you will need to start this service.  What is the service?  From Microsoft:
Enables generic input access to Human Interface Devices (HID), which activates and maintains the use of predefined hot buttons on keyboards, remote controls, and other multimedia devices. If this service is stopped, hot buttons controlled by this service will no longer function. If this service is disabled, any services that explicitly depend on it will fail to start.

So... The default in Windows 2012 is that this service is set to Manual(Trigger Start) so if your device does not have software to trigger this service, like my you will need to start it manually. I set my service to start automatically, so that I won't loose my buttons when I restart the server.

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