I've spent a far amount of time talking about how access rights work in Exchange 2000, and how they worked in Exchange 5.5.
Now I'm going to focus on how we put all the pieces together to make this whole thing work. Hang on tight, it's a bit of a wild ride.
First off, I'm going to assume that you understand how NT's access check mechanism works. As a quick refresher, here's the meat of the access check process.
Every object has a security descriptor. The security descriptor contains 4 pieces of information. The "OWNER", "GROUP", "System Access Control List" and the "Discretionary Access Control List". The Access Control Lists (ACLs) define access rights to the object, the System ACL describes the auditing semantics of the object (whether or not access to the object is audited or not). The Discretionary ACL defines who can have access to the object.
An ACL is an ordered collection of Access Control Entries (ACEs). Each ACE contains four pieces of information: The type of the ACE, flags for the ACE (more on these later), the access rights that are controlled by the ACE, and the SID of the security principal to whom the ACE applies (a security principal is NT security-talk for a user or group).
There are in general, two types of ACEs that will appear in a discretionary ACL: ACCESS_ALLOWED ACEs and ACCESS_DENIED ACEs. What a surprise - ACCESS_ALLOWED ACEs grant access to an object, and ACCESS_DENIED ACEs deny access.
A quick digression: This is an over simplification of ACEs - there are actually 4 types of ACEs that appear in a DACL, but they don't affect this discussion (but I do feel obligated to point this out :)). At some time in the future I'll discuss object ACEs and why they're so utterly cool.
When NT does it's access check, it iterates through the ACEs in the DACL for the object, checking to see first if the principal in the ACE is active in the users token. In other words, if the SID in the ACE is the SID of the user making the check, or if the SID is for one of the user's active groups (in Windows 2000 and above, group membership can be excluded from access checks).
If the principal in the ACE is active in the users token, it next checks to see if the access rights that are being requested are included in the access mask. If they're not, it skips to the next ACE. If they are, NT next checks the ACE type. If it's a grant ACE, then it turns off the bits in the desired access mask. If it's a deny ACE, then it denies access.
When the access check code gets to the end of the ACEs, it checks to see if any of the requested access rights are still turned on - if that's the case, it denies access (because one of the desired access rights wasn't granted by an ACE).
Ok, with that behind us, now it's time for the ride to start.
Remember that Exchange 5.5's access check is a most-specific to least specific access check. This means that first exchange looks to see if a user is specifically mentioned in the ACL, then if a group the user's a member of is in the ACL, then it tries the rights for the user "everyone".
So if we're going to use NT's access check mechanism, we want to model the ACLs after the Exchange ACLs. We're going to want to break up the ACL into three sections:
- Section 1 - The rights granted to USERS.
- Section 2 - The rights granted to GROUPS.
- Section 3 - The rights granted by default to the folder.
And we want to put them in that order - since NT's access check evaluation goes in order, we want to emulate the ordering that Exchange uses.
First let's consider section 3, since it's easiest. Section 3 can be implemented with a single ACE that grants access to the "EVERYONE" user.
Cool, that's 1/3rd of the problem done :)
The next thing to tackle is the rights to users. The Exchange semantics for users says that if the user is in the ACL, then the exchange ACE defines the complete set of rights for that user. So we clearly need a grant ACE that grants access to the user in the Exchange ACL.
But what if the exchange user is a member of a group that's listed further down in the ACL? We don't want the access check to continue and find those ACEs, since the user wouldn't have those rights in Exchange 5.5. So we've got to find a way of "stopping" the ACL evaluation. Hmm... It is a puzzlement.
But wait! What about a deny ACE? As I mentioned above, if the rights listed in a deny ACE are requested, then the access check function will stop evaluating immediately. So what if we use a PAIR of access rights. One that grants the user the rights that correspond to the Exchange 5.5 rights, and the other that DENIES that user every right that isn't granted. In other words, and ACE that's just there to force the access check logic to terminate. That will work!
So we're 2/3rds of the way there. For section 1, we take each user mentioned in the Exchange 5.5 ACL and create a pair of ACEs - one that grants the user the rights that correspond to the rights they have, and a stopper. For section 3, we add an ACE that grants Everyone the rights that correspond to the "default" rights. Now what about groups.
Groups function additively in the Exchange access check mechanism. The user is granted the access rights granted to all the groups they're in (so if the user is in groups A, B, and C, and the ACL grants group A frightsReadAny access and group C frightsWriteOwn access, the user gets frightsReadAny|frightsWriteOwn access).
Well, the NT access check mechanism does that - it iterates through access allowed ACEs trying to see if each of the rights in the ACE is granted to the user. So we can just turn the group ACLs into access allowed ACEs and just put the access allowed ACEs together.
But what about a situation where a group is allowed frightsReadAny rights to a folder but default was granted frightsWriteAny (a silly scenario, but possible)? In this case, the user wouldn't have write any access to the folder, they'd only have read any access. So we need another set of "stopper" ACEs. But we need to put the stopper ACEs AFTER the last of the group ACEs, and we need to put them in a group together before the final default ACE.
Phew, that's a lot. So the bottom line is that when we convert the Exchange 5.5 ACL to an NT security descriptor, we need to form an ACL with the following format:
GRANT User <A> rights
DENY User <A> Inverse(rights)
GRANT User <B> rights
DENY User <B> Inverse(rights)
GRANT Group <G> rights
GRANT Group <H> rights
DENY Group <G> Inverse(rights)
DENY Group <H> Inverse(rights)
REPEAT <0 or 1>
GRANT Everyone rights
An ACL that has this form is called an "Exchange Canonical ACL".
So to convert an Exchange 5.5 ACL to an Exchange 2000 ACL, the first thing to do is to sort the Exchange 5.5 ACL into three groups: Users, Groups, and default. For each user, create an NT ACCESS_ALLOWED_ACE that grants the Exchange 2000 rights that correspond to the user's 5.5 rights. Then add an NT ACCESS_DENIED_ACE that denies any rights that weren't allowed. Next, for each group, add an ACCESS_ALLOWED_ACE for the group (with the corresponding rights). Now loop through the groups again, and add an ACCESS_DENIED_ACE for each ACE listed above. And finally, if the Exchange 5.5 had a default ACL, add an ACCESS_ALLOWED_ACE granting EVERYONE the rights that correspond to the default rights.
So we're done, right?
Nope. I did say this was a bit of a roller coaster ride.
Remember that the Exchange access rights were broken down into two sets - the rights that apply to folders and the rights that apply to the messages in that folder. Unlike the native filesystems, Exchange effectively had dynamic inheritance. A quick aside: dynamic inheritance means that when the ACL of the parent object (the folder) changes, the ACL of the objects contained in the folder also change. The Active Directory implements dynamic inheritance, NT's filesystems implement static inheritance.
In the original design of the Exchange ACL conversion mechanism, the folder actually had TWO ACLS on it - one was the folder ACL, which was constructed as described above. The other ACL was called the "default message ACL". The idea was that for any message that didn't have an ACL, we'd go to the folder containing the message, retrieve the default message ACL on that folder and use that instead.
Unfortunately, the test team felt that this was too confusing, and they were concerned that the two ACLs could get out-of-sync (a very valid concern btw). So we decided to combine the folder ACL and the default message ACL into a single ACL.
How could we do that? We overloaded the access rights in the Exchange 2000 access rights - some of the bits apply to the folder, and the same bits apply to messages in the folder!
Well, it turns out that the NT ACL design comes to our rescue. Remember how I mentioned flags in the ACE above? It turns out that there are 5 ACE flags:
Now we don't need to consider most of the flags, but there are a couple of interesting flags. In particular, there's the INHERIT_ONLY flag which indicates that the ACE should be ignored on an access check - cool! We have a way of "hiding" ACEs from AccessCheck. The next ACE flag of interest is the OBJECT_INHERIT_ACE - this indicates an ACE that applies to objects in a container (or messages in a folder!).
Aha! So we can do this - we take all the ACEs that would normally have been put in the default message ACL and we put them into the folder ACL with the OBJECT_INHERIT_ACE flag AND the INHERIT_ONLY_ACE flag. Then when we're calculating the ACL for a message that doesn't have it's own ACL, we can ask NT to run the ACL inheritance algorithm over the folder's ACL to come up with ACL for the message! Voila!
So are we there yet? Well no, not quite.
The idea of paired rights above is pretty cool. But there's a bit of a problem. It turns out that there are a number of tools like CACLS that really don't like ACEs with 0 access rights in them. And if a user (or group) is granted full rights above, then the "stopper" ACE will have 0 rights. So the rules for forming the ACL were relaxed slightly - if the set of rights granted or denied was 0, then the ACL conversion logic would omit the 0 rights ACE.
And that, in a nutshell is how the wacky Exchange 2000 folder ACLs came to be.