Search This Blog

Sunday, January 13, 2013

Exchange 2007 Admin in C# on W2008R2 using Visual Studio 2010

I wrote a service which performed Exchange 2007 administration tasks which performed successfully for years running on a Windows 2003 R2 server.  Recently we went through an upgrade to W2008R2 and Exchange 2010 and I began upgrading the service to use the new and very different PowerShell snap-in for Exchange 2010.

Suddenly we partially reversed course, however, and decided to stay with Exchange 2007 while continuing the upgrade to W2008R2 for the server OS.

So I am having to rewrite my original windows service code to run on 64-bit W2008R2 machines.  I remembered that when I initially wrote the service I had several issues getting Visual Studio (then VS2005, now VS2010), the PowerShell runspace, the Exchange 2007 Administration PSSnapin, and the bit-iness of the machine to agree and to let me do what I needed to do.  I remembered that these problems took days to resolve.

I would run into persistent problems getting my C# code to recognize the snap-in, to use the correct version of the System.Management.Automation tools, etc. which would be evidenced with errors like:

"No snap-ins have been registered for Windows PowerShell version 2"

"Could not load file or assembly 'Microsoft.Exchange.PowerShell.Configuration, Version=14.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' or one of its dependencies. The system cannot find the file specified."

"MSCorlib.dll is compiled for wrong processor"

Googling around it is clear that this is a common problem and is a bit-iness issue between all the different entities and all of them must be lined up in the right way to make things work.

I established a set of steps that worked for me in a virgin test environment on a virtual machine; I then took it into my production environment (which had suffered installs and uninstalls of the Exchange 2007 and 2010 management tools) and tried it again and it worked.

The steps I followed to get it to work were:

01) Install the Exchange 2007 management tools from the Exchange 2007 SP1 distribution DVD onto the server
02) Update the management tools to service pack 3 (by downloading the Exchange 2007 SP3 download from Microsoft)
03) Start Visual Studio 2010 and create a console application. 
04) Create a class in the console app to hold the Exchange 2007 Admin PowerShell wrapper written by Nick Smith (at http://knicksmith.blogspot.com/2007/03/managing-exchange-2007-recipients-with.html)
05) Have the main() method of the program.cs file call Nick Smith's exchange wrapper, e.g.:

  static void main(string[] args)
  {

    // Do a simple dump of mailbox names
    ExchangeManagementShellWrapper ems = ExchangeManagementShellWrapper.Instance;
        ICollection results;
        results = ems.Runsp
aceInvoke("Get-Mailbox");
        foreach (PSObject item in results)
        {
              Console.WriteLine(item.Members["Name"].Value.ToString());
        } 
  }


06)  Make sure the project's set to build using .NET framework 3.5.
07)  Make sure the project's set to build to the x64 platform
08)  Make sure there's a reference to "C:\Windows\Microsoft.NET\Framework\v2.0.50727\System.Management.dll"
09)  Make sure there's a reference to "C:\Program Files (x86)\Reference Assemblies\Microsoft\WindowsPowerShell\v1.0\System.Management.Automation.dll"

That should do it.  The Exchange 2007 PowerShell cmdlets are only installed as x64 and the System Automation dlls only seemed to work for me if the code is targeted towards .NET 3.5, not .NET 4.0 (which my VS editor defaulted to).

I'll include the source code for the project if there's any interest in that.

Sunday, November 4, 2012

MS Active Directory "accountExpires" value -- getting and setting it

If you use ADSI to get a DirectoryEntry object from Active Directory you'll find that the "accountExpires" property is stored as a DateTime variable in a large integer format called the IADsLargeInteger format (http://msdn.microsoft.com/en-us/library/windows/desktop/aa706037%28v=vs.85%29.aspx).  The format represents a date as an 8 byte variable indicating the number of 100-nanosecond intervals since January 1, 1601 (look up the Wikipedia article for this date if you're curious as to why it's used).

Converting this format into a standard C# DateTime value requires a 2-step process of converting the IADsLargeInteger into a standard C# long value, then converting this long value into a C# date time.

I found several (incorrect) versions of functions to accomplish this on the Internet but the best functioning version (derived from MSDN's articles on accountExpires, IADsLargeInteger, and DateTime conversions) comes from Tobi's 'tips' 4 and 5 in his notes on Active Directory (http://www.fsmpi.uni-bayreuth.de/~dun3/archives/category/it/programming/active-directory):

First, convert the IADsLargeInteger into a long:

private static long ConvertLargeIntegerToLong(object largeInteger)
{
Type type = largeInteger.GetType();
int highPart = (int)type.InvokeMember("HighPart", BindingFlags.GetProperty, null, largeInteger, null);
int lowPart = (int)type.InvokeMember("LowPart", BindingFlags.GetProperty | BindingFlags.Public, null, largeInteger, null);

return (long)highPart <<32 lowpart="lowpart" p="p" uint="uint">}

Then use this to convert the long into a DateTime.  Note that the DateTime is stored in UTC format -- luckily .NET has the "FromFileTimeUTC()" method ready-made to handle this for us:

object accountExpires = DirectoryEntryHelper.GetAdObjectProperty(directoryEntry, "accountExpires");
var asLong = ConvertLargeIntegerToLong(accountExpires);
    
if (asLong == long.MaxValue || asLong <= 0 || 

DateTime.MaxValue.ToFileTime() <= asLong)
{
return DateTime.MaxValue;
}
else
{
return DateTime.FromFileTimeUtc(asLong);
}

   
Note 1:  "Magic values" in the accountExpires attribute

Two values in the accountExpires attribute indicate that the account is set to "Never Expires."  These are zero (0) and  9223372036854775807 (0x7FFFFFFFFFFFFFFF).
  
See http://msdn.microsoft.com/en-us/library/windows/desktop/ms675098%28v=vs.85%29.aspx

Note 2:  Getting the accountExpires attribute from an ADSI "DirectorySearcher" search result

This is very important (and delayed me quite a while when I didn't take it into account).

If you pull an account's DirectoryEntry using ADSI the 'accountExpires' property is returned as an IADsLargeInteger, but if you get it as a search result using "DirectorySearcher" the property will be presented as a standard long.  No translation from IADsLongInteger are required, you'll just need to do the translation from long to DateTime ("step 2" above).

See http://forums.asp.net/t/999913.aspx and comments by MVP "Dunry" for a useful discussion of this.

Wednesday, October 31, 2012

How to lock an Active Directory account with C#

There are a lot of answers out there to how to lock an account using ADSI -- some just wrong, others dangerously wrong.

This one works cleanly, transparently, and well:

private void LockAccount()
{

string _userAccountWithoutDomain = “test”;
string _domainName = “IND”;
string _userBadPassword = “yyyyy”; // password should be incorrect
int _passwordExpiryPolicy = 3;
string _connectionPrefix = “LDAP://” + _domainName;

for (int i = 0; i < _passwordExpiryPolicy; i++)
{

try {
new DirectoryEntry(_connectionPrefix, _userAccountWithoutDomain, _userBadPassword).RefreshCache(); }
catch (Exception)
{ }
}
}

Thanks to Sanjiv at http://sanjivblog.wordpress.com/2011/05/13/how-to-lock-the-ad-active-directory-account-programmatically-in-c/

Friday, October 26, 2012

Error message "Element 'link' cannot be nested within element "

I keep getting this wrong -- when the error message indicated in the title appears it's because I've stuck the link directive (used for bringing CSS files into an HTML document) in the wrong place.

Short answer:  The "link" directive can only be used inside the "head" tag of an HTML document.

jQuery error: "$ is undefined"

When using Visual Studio 2010 and coding in ASP.NET where I'm using one of the various jQuery plug-ins (a datepicker, the BlockUI page blocker utility, or the jQuery validation tools) I'll occasionally get the JavaScript error featured in the title -- it simply means that the page fired up and it couldn't resolve the "$" synonym for "jQuery."

Invariably I'll check and make sure that I've got the jQuery code "included" in the web page (which I do) but if I look at the page using Firefox "Firebug" I'll see that something will have hosed up the syntax of the include statement (based on the browser, whether I'm using master pages, nested master pages, etc.).

Being lazy I like to include files by dragging them from the "Solution Explorer" pane of Visual Studio right into the ASP.NET/HTML code.  I always hope that VS2010 will just "figure out" the right syntax for me, and so long as I'm not using master- or nested-master pages I'm generally right.

Once masterpages are introduced, though, things get screwed up.

Here's a syntax solution I've found that seems to work pretty well:

Rather than:

<script src='~/scripts/jquery-1.8.2.js' type="text/javascript"></script>

I use:

<script src='<%= ResolveUrl("~/scripts/jquery-1.8.2.js") %>' type="text/javascript"></script>

It seems to circumvent the path-naming-weirdness introduced by ASP.NET-masterpage-wrapping.

Saturday, October 13, 2012

My persistent 500.24 error

Because of my client's NT trust relationships my websites need to use NT credentials impersonation quite frequently and I keep falling into the same trap when setting up impersonation in IIS7 (rather than IIS6, which I was kind-of used to).

I build and push out the website and then try to access a diagnostic page (which shows the current website's users credentials as understood by the web server) and get the error:

"HTTP Error 500.24 - Internal Server Error An ASP.NET setting has been detected that does not apply in Integrated managed pipeline mode"

A couple things will float up in my mind right then -- I know that in IIS6 and older versions of IIS I always put "impersonate=true" into the web.config file and this causes problems with the new pipeline mode of IIS7.  I'll knock this line out of the web.config file and it won't help -- the 500.24 error will disappear but impersonation is gone.

Then something will happen to throw me into a blind panic (usually a required demo in ten minutes or some other "show and tell" nonsense that means I've got to get this resolved fast) and I'll start flailing around trying to fix it.  I always do, but I want much less stress in my life -- so here are the basic steps again as a reminder so the experience can be shorter next time:

- Go into IIS7 manager and reset the website's application pool to use the "Classic" pipeline
- Modify the web.config file to include:

<system.webServer>
   <validation validateIntegratedModeConfiguration="false"/>
</system.webServer>
 
 See these links for more (and better) information:

 

(The latter has a nice section that explains this error very well.)

My persistent website 401 error

This is a reminder to myself.

I work in an NT environment with multiple domains (e.g. domainA, domainB, and domainC) where one domain ("domainC") trusts the other two domains.

I keep my IIS7 website in domainC and want users in any of the domains to be able to access it using their domain's credentials.

I make sure to disable anonymous access on my website and enable integrated windows authentication so that users will come into the website using their current credentials.  

The roadblock I experience each time is that I'll set up the website and test it using an account in domainC and all will be well.  During final testing I go over to domainB or domainA, log in, and try to get into the website and run into a "401" error. 

I always do this:  I'll go back to the website and try and reset permissions to the website's directory to allow "domainA\Everyone" and "domainB\Everyone" to have access, but for some reason I won't be able to.   I'll remark again that this works in my personal devlab but not in the client's environment.  I'll remember hearing that there's some sort of firewall blockage on LDAP calls between domains in this environment.

I'll try to create a domain local group in domainC and add the domainA\Everyone and domainB\Everyone groups into a local group I can give website rights to, but that won't work either.

I'll get frustrated here and start Googling and 4-5 hours will disappear as I learn how little I really know about security (and pursue several blind-alley solutions proposed by some other clueless bloggers).

I'll then spend another hour dicking around with every setting conceivable on my website, and then create a small sample "WhoAmI" website that just returns the current user's credentials.  Setting security on this test website, I'll turn off anonymous access, turn on nt authentication, and then give the local group WEBSERVERNAME\Users all rights to the website and discover that I can now access it from all the trusted domains.

The lesson here is to start by giving the WEBSERVERNAME\Users local group basic rights to the website just to get started.

This has cost me several hours to recreate twice -- Note to self:  don't do this again.