>
Blog
Book
Portfolio
Search

12/1/2007

6882 Views // 0 Comments // Not Rated

Accessing AD Data From SharePoint

I've been hitting the forums up lately to see if I can make the world a better place one SharePoint issue at time. Well, apparently, this SharePoint world is in really bad proverbial shape, and one of the largest oppressors is Active Directory. "How do I get AD information in SharePoint?" Ugh. AD has always reminded me of an old Oldsmobile. It'll get you from point A (not authenticated) to point B (authenticated) and even to point C (authorized) without any fuss.

But as soon as you hit the gas too hard, or don't let it warm up long enough in the winter, or forget to clean the gunk out of the engine, it'll start fussing big time. I liken this (poorly) to accessing AD programmatically. AD works great under the covers of Windows Auth, but man alive is it a pain in the ass to code against. And that's not even considering the fact that we are throwing SharePoint into the mix!

Anyways, there are two main ways to think about AD in SharePoint. The first is more "legit" and the second is more powerful. For the first way, remember that out of the box, SharePoint 2007 uses Windows Auth to authenticate its users, which is all AD behind the scenes. Therefore, whenever you access information about users through the SharePoint API, you are essentially dealing with SharePoint's existential wrapper around AD.

But that's also part of the problem: in SharePoint's "user" layer, other entities are added, such as the concept of SharePoint groups, SP-object permissions, etc. But regardless, it's all sitting on top of AD. This is why you can use the Central Admin tools to actually map AD properties to SharePoint User Profile properties.

So if you need cursory information about the current user's AD properties, or if a certain user is in a group, a lot of times you need to go no further than the SharePoint API. Here's some code:

Code Listing 1

  1. //get the current SharePoint user...
  2. SPUser user = SPContext.Current.Web.CurrentUser;
  3. //...or all users (local to the specified site collection)...
  4. using (SPSite site = new SPSite("http://server"))
  5. {
  6. using (SPWeb web = site.RootWeb)
  7. {
  8. foreach (SPUser user in web.AllUsers)
  9. {
  10. ...
  11. }
  12. }
  13. }
  14. //...and you've got AD information!
  15. string email = user.Email;
  16. string loginName = user.LoginName;
  17. string displayName = user.Name;
  18. byte[] objectSid = user.RawSid;

So as you can see, sometimes in SharePoint, AD is not very far away. Other times, however, it's really damn far away, and it's cold outside, and your socks are wet. For example, you can't get AD group information "for free" like you can with user data. SharePoint groups are a completely different construct, and proprietary to SharePoint itself. And if you're using a different provider mechanism other than AD, then there is no link to AD through SharePoint.

So now to the real meat and potatoes of the issue at hand: how do I get straight up raw Active Directory information into SharePoint? Well, you write some code. The basic architecture I'm going to use first creates a utility class to provide methods that perform specialized AD functions, such as getting all groups for a certain user, or all users in a certain group.

Next, I'm going to use a web service to expose these calls. Why a web service? Because AD is a data store, and SharePoint is a UI, and ne'er the twain shall meet. Also, because SharePoint 2007 has made great strides beyond 2003 in terms of not taking over IIS. We can nestle a custom web service under one of our existing SharePoint IIS web sites, and it works just fine within SharePoint's context without interfering with it.

(The only action we might have to take is to allow ASMX files to be served through this particular web application. This is much easier than dealing with exclusions back in 2003. Fire up Central Admin, and in the "Security Configuration" section under the "Operations" tab, click "Blocked file types." Pick your web app, delete "asmx" from the listbox, and click "OK.")

Finally, I'll consume the web service on a web part (hosted in a SmartPart). But don't think I forgot about you InfoPathers out there! I designed this web service to also be easily used in InfoPath as well, and I'll hit on this at the end of this article. The UI aspect of this solution is actually arbitrary, as web services can (beautifully) be consumed just about anywhere.

The first part of the puzzle is the AD utility. I won't go into too much technical detail here, since it's outside the scope of this article. However, I'll provide all the AD code so that you can use it for your purposes. What I will delve into, without further ado, is the security ramifications of dealing with Active Directory through SharePoint.

In order for your code to hit AD, you need two things. First is a domain account that has permissions to (at least) query AD, and possibly the rights to modify it. I think this is a little scary through code, unless your administrators have plenty of life insurance. This domain account is used in conjunction with a URL and passed to the constructor of a DirectoryEntry object (in System.DirectoryServices). This object is our entry point into the forest.

But before this code will have a chance in hell of operating in SharePoint, we need to consider the second requirement: Code Access Security (CAS). AD code needs the DirectoryServicesPermission to run. This is a failry beefy permission, and I've never seen it included by default in any policy. Now when it comes to CAS, there are two main paradigms that I've been pulled between: the "right" way and the "oh, F it" way.

I'm not going to lie: the right way sucks. I mean it's not THAT bad, but you have to screw around with XML and config files and all of that. Now don't get me wrong: by using CAS and the IPermission interface stuff, you can explicitly assign your code only the permissions that it needs to run. This is nice and clean and auditable and if anyone from Microsoft is reading, then it's certainly the way I'd recommend going about this. For more read this.

But I'm a developer. I'm also a consultant. At the intersection of these two things are code, requirements, and even architectures that change frequently. To maintain CAS appropriately in the tumultuous environments I find myself operating in would mean a lot of overhead for me and a lot of cost for my clients. So, with that pretense, please indulge me in the "oh, F it" paradigm before we get to the code.

I know that it's bad news to let your code run rampant on the server, doing whatever the heck it wants. CAS is in place to prevent this. But nothing is stopping us from adding SPSecurity.RunWithElevatedPrivileges delegates around all our code to have it run under the app pool's identitiy. Nothing is stopping us from dropping our assemblies in the GAC. Nothing is stopping us from making the trust levels of our web apps full.

Nothing! If you can physically (or logically) get to the server to deploy your code, then someone more important than you are is implicitly trusting your code to run by trusting you to be able to deploy it. This is not to say that our code as devlopers is an extension of our itegrity as people, but in a way it is. So why make it harder? If I have permission to modify CAS, then I have permission to circumvent it as well. And I'll tell you that at the end of the day, the world is just as secure, and maybe you even had time to implement one additional feature in your app.

Ranting aside, the two security measures I took for this code is to deploy the AD utility assembly to the GAC and to grant the web service full trust via the web.config. What I do NOT recommend is granting your entire SharePoint web app full trust. The problem with that approach is that you are allowing way too much functionality that you didn't write to have god permissions.

Dropping the AD utility in the GAC makes sure that it has the permissions to do what it needs to do. I granted my web service full trust so that it can call the AD code and get around any partially-trusted caller issues. And that's all. When writing code against internal servers, I hope you can agree that messing around with CAS is just...well...oh, F it.

Here's how to get it up and running:

  1. Drop the utility class DLL into the GAC on the server.
  2. Update the web service's web.config to match your AD environment.
  3. Publish the web service code to the SharePoint server, which needs to be in the same domain as AD.
  4. Create a new virtural directory under your SharePoint web app and point it to the web service.  
  5. Recycle the web app's app pool.
  6. On the UI project, update the web reference to point to your web service.
  7. Build the user control, and deploy it the server.  Set it up as a SmartPart.
  8. Once it's working, feel free to modify the code to do what you need it to.

In InfoPath, create a new data connect that receives data from a web service, and point it to the URL. Enter your password 900 times (has anyone noticed InfoPath doing this), pick the web method you want, and finish the wizard. Then using standard InfoPath rules logic, pass the data connections values from the form, and display the results in a repeating control.

And speaking of InfoPath, for the feint of code, you can directly hook into SharePoint's profile service to get user data in your forms. Remember, in most cases, SharePoint user data is AD data. Using the same procedure in the above paragraph, add a URL to http://server/_vti_bin/UserProfileService.asmx, and you'll have plenty of web methods to access user-specific SharePoint data.

Finally, as an addendum to the code, I'll list the names of the indexed property of the SearchResult.Properties collection. This is handy for getting data about the user or group of a query result. Since we also have an Exchange server on our domain, you'll see that it's modifications to the forest schema are available as well.

  • objectClass
  • cn
  • sn
  • c
  • l
  • st
  • title
  • postalCode
  • givenName
  • distinguishedName
  • instanceType
  • whenCreated
  • whenChanged
  • displayName
  • uSNCreated
  • memberOf
  • uSNChanged
  • co
  • department
  • company
  • homeMTA
  • proxyAddresses
  • homeMDB
  • mDBUseDefaults
  • mailNickname
  • name
  • objectGUID
  • userAccountControl
  • badPwdCount
  • codePage
  • countryCode
  • badPasswordTime
  • lastLogon
  • pwdLastSet
  • primaryGroupID
  • userParameters
  • objectSid
  • accountExpires
  • logonCount
  • sAMAccountName
  • sAMAccountType
  • showInAddressBook
  • legacyExchangeDN
  • userPrincipalName
  • objectCategory
  • mSMQSignCertificates
  • mSMQDigests
  • msNPAllowDialin
  • dSCorePropagationData
  • textEncodedORAddress
  • mail
  • manager
  • msExchHomeServerName
  • msExchALObjectVersion
  • msExchMailboxSecurityDescriptor
  • msExchUserAccountControl
  • msExchMailboxGuid
  • msExchPoliciesIncluded
  • msRTCSIP-PrimaryUserAddress
  • msRTCSIP-UserEnabled
  • msRTCSIP-PrimaryHomeServer
  • msRTCSIP-FederationEnabled
  • msRTCSIP-InternetAccessEnabled
  • msRTCSIP-ArchivingEnabled
  • msRTCSIP-OptionFlags
  • nTSecurityDescriptor

2 Tags

No Files

No Thoughts

Your Thoughts?

You need to login with Twitter to share a Thought on this post.


Loading...