Miscellaneous Debris

Avner Kashtan's Frustrations and Exultations

June 2004 - Posts

Disposable Impersonation

Impersonating a user in .NET isn't quite as simple as it could be. It's really easy if you you already have a WindowsIdentity or WindowsPrincipal in hand, but to get a handle on an arbitrary user you have to resort to P/Invoking the LogonUser and DuplicateToken APIs to get an impersonation context given only a username and password.

Easy enough to wrap in your own function, if you need to use it, but then you have to remember to Undo() the ImpersonationContext when you're done. Not that bad, but could be neater.

So the solution I came up with today is this:

Create a new class, call it ImpersonationContext, and add our Impersonation code in the constructor - pass the username/password as parameters. The WindowsImpersonationContext object we'll save in a private variable.
Next we'll implement IDisposable in our class and call the WindowsImpersonationContext.Undo() in the Dispose method.

What does this give us? A rather clean and readable way to run a block of code using a different security context, with automatic reversal of the impersonation when we're done.

using (new ImpersonationContext(username, password, domain)
{
      // Run my impersonated code.
}

(Sorry for not bringing actual code - this was written on a closed network so I don't have the sources available)

Posted: Jun 30 2004, 10:37 PM by AvnerK | with 10 comment(s)
Filed under:
COM Add-Ins : Multiple Madness!

[Don't you hate it when IE throws your long post to the trash? We need a smart client for updates!]

COM AddIns for Office are a nice feature. A rather easy way to add some buttons and functionality to your documents, and even deployment isn't too much of a hassle.

But I ran into a problem today when I had two Word documents open at once. It seems that the button was loaded for both, but was only functional for the first instance. The second instance had something that looked like a button, got clicked like a button, but all-in-all did nothing that buttons did - that is, trigger an event.

Several hours of furious debugging followed. VB6 code was translated to VB.NET and then to C#. No go. Code was moved from the OnConnection event to OnStartupComplete - still nothing. A working sample retrieved and compared - nothing relevant seemed to pop up from the page.
Only after going line-by-line over the sample and copying it to my code did we get an answer:

VB controls and similar APIs tend to have a Tag property for all their controls. A convenient, loosely-typed way to store extra data about the control without having to subclass the control or design your own interface.
A great workaround for lazy programmers, up until the time it bites you back. It bites you when you have to go over someone’s code where he uses properties for data other that their intended use. It bites you when you have a “Title” field for a mailbox that is actually used to store an organizational Department. It bites you when your code doesn’t document itself – it does something other than what you expect it to do.

And that’s exactly what Office does – it uses the Tag property of the CommandBarButton to keep track of control instances between multiple open documents.
It doesn’t matter what the Tag holds – the minute you put something in it, the button works fine on as many instances of Word as you’d like.

Why does it rely on the generic, optional, misleading Tag property for this? Couldn’t they add some internal property? Some automatic mechanism? Why do I have to set a random, pointless string to an arbitrary, pointless property to get functionality to work?

Lazy workaround for Lazy programmers. Bah.

Posted: Jun 17 2004, 05:52 PM by AvnerK | with 6 comment(s)
Filed under:
Office 2003's disappearing-reappearing PIAs.

Just in case someone else is running into this problem:

If you're trying to install Office 2003 Professional and want to make sure you install the PIAs as well, you go to the setup components list and look for .NET Programmability Support and are surprised when you just can't find it - make sure you have the .NET Framework 1.1 already installed on the computer before you install Office. It won't show up on the list, and won't get installed, if the framework isn't already there - and it'd be a shame to go and reinstall Office now, wouldn't it?

Posted: Jun 10 2004, 01:35 AM by AvnerK | with no comments
Filed under: ,
Ghost Files

For some obscure reason, Windows Sharepoint Services allows me to fetch a non-existent file.

Consider this code:
SPSite site = new SPSite(site_url);
SPFile file = site.RootWeb.GetFile(file_url);

This code will return an SPFile object regardless of whether file_url points to an existing file or not.
To find out if the file exists, you need to call the file.Exists() method.
Even if we ignore the fact that it's against the common practice of throwing exceptions for these situations, it still behaves oddly.

You would expect that the properties for a non-existent file would be null, or at least throw exceptions - and indeed, most of them do throw (more on that later), but some of them DON'T - properties like the Name and URL of the file return values that are derived from the original URL I passed it, even though they do not, in fact, point to an existing file.

And the other properties DO throw, but they either throw a System.ArgumentOutOfRangeException, which makes absolutely no sense, or else a SPException specifying that the document library doesn't exist. The same property can throw a different one of the above exceptions, depending on whether the URL we gave it points to a non-existing file in an existing doc library, or to a doc library that doesn't exist at all.

It's a mess, I tell you - a mess.

Posted: Jun 10 2004, 01:31 AM by AvnerK | with 1 comment(s)
Filed under: ,
Who Am I?

As we probably know, the most popular activity involving the Active Directory for the developers amongst us is to check for authorization. We get a username from the context/environment/browser, and now we wanna check if the user is allowed to do whatever. We may use the role-based security solutions provided by COM+ or .NET or whatever solution we want, and they are nice and fine and good, but there comes a time in every programmer's life when he must lay down the line and say “That's it. I'm going in there. Just me and System.DirectoryServices. If I'm not out in 30 minutes, contact my administrator”.

So first, a word of warning - System.DirectoryServices is a COM wrapper. It is NOT managed code. Windows has a lovely library for accessing the AD and other LDAP providers, and had it for years - ADSI in its various version and incarnations. A nice API, all in all. DirectoryServices in the BCL is just a set of wrappers over ADSI, not a brand-new implementation of LDAP. This means that there is a lot of COM interop going on in the background. This shouldn't be too worrisome, since COM interop isn't a bad thing, but it is important to dispose what you instantiate and make sure you don't leave things lying around.

There, having established that, we will quickly learn to create a DirectoryEntry for our user (either directly or using the DirectorySearcher - no need to elaborate there (though I can, if people ask me to)) and we can now check the properties for the “memberOf” property and see what groups he is a member of. This works well, except that we only get the groups we are DIRECT members of. If we want a complete list of nested groups, we have to start recursing and iterating, and this takes a LONG time. Especially when we have a lot of groups.

So what we do? We use a little-known and barely-documented feature of our Windows authentication architecture.
You see, when we try to access a network resource, for instance, the resource has to know whether we are authorized or not. It does this by receiving from the client (our user) a set of security credentials that contain the Security IDs (SIDs) of all the groups we belong to, whether directly on indirectly - we can't have Windows recurse through the AD whenever we try to access some folder, right?

So we'll use the same mechanism. Among the many properties available for a DirectoryEntry object in the ActiveDirectory is a property called “tokenGroups” which contains a list of all SIDs of all security groups the user belongs to. This list is flat, since it is used for direct authorization only, not for complex rules inherent in our organizational hierarchy. This list of SIDs can then be translated into group names for our enumerating convenience, or perhaps we should just get the SID for the group we are checking and see if it is the list.

 A few points of interest:

1) Since the tokenGroups property is relatively heavy, it is not automatically loaded when we create a new DirectoryEntry. To do so, we will call the entry's RefreshCache() method to explicitly load that property:

DirectoryEntry de = new DirectoryEntry(my LDAP path);
de.RefreshCache(new string[] {“tokenGroups“});

2) Once we loaded the property, it is represented as an array of byte arrays, so we will get it like this:

PropertyValueCollection tg = de.Properties["tokenGroups"];
foreach (byte[] SID in (Array)tg.Value)
{
   // Translate the SID, or whatever.
}

1) To get a group name from a SID (which is basically an array of bytes), we'll have to do a little Win32 interop using the LookupAccountSid function.

enum SID_Types
{
   SidTypeUser = 1,
   SidTypeGroup,
   SidTypeDomain,
   SidTypeAlias,
   SidTypeWellKnownGroup,
   SidTypeDeletedAccount,
   SidTypeInvalid,
   SidTypeUnknown,
   SidTypeComputer
}

[DllImport("advapi32.dll")]
private static extern bool LookupAccountSid(
       string lpSystemName,
       byte[] lpSid,
       StringBuilder lpName,
       ref int cchName,
       StringBuilder lpReferencedDomainName,
       ref int cchReferencedDomainName,
       ref SID_Types peUse
);

private string getNameFromSID (byte[] SID)
{
  
int
GroupLength = 256;
   int DomainLength = 256;
   StringBuilder Groupname = new StringBuilder(GroupLength);
   StringBuilder Domainname = new StringBuilder(DomainLength);
   SID_Types AccountType = SID_Types.SidTypeUnknown;
   LookupAccountSid(null, SID, Groupname, ref GroupLength, Domainname, ref DomainLength, ref AccountType);
   return Domainname.ToString() + @”\” + Groupname.ToString();
}

Posted: Jun 01 2004, 09:22 PM by AvnerK | with 1 comment(s)
Filed under:
More Posts