Access Control in .NET
Created: 30 March 2008
As of v2 of .NET you can get and set the security of securable objects such as files, folders and registry keys.
Other object types can be added as needed. While all the access control
objects derive from the same base classes they share only a few common elements.
Therefore working with the security of a file requires using different classes
than registry keys. This is an unfortunate limitation of the existing
implementation since a generic security handler can not be created with the
existing objects. This article will discuss working with the access
control classes in .NET. While the article will use folders as the target
object the concepts (but not the classes) apply to any securable object.
To provide a graphical representation for this discussion open the properties
for a folder in Windows Explorer and go to the Security tab. Click the
Advanced button to gain access to the more advanced features we will discuss.
All the core access control types are contained in the
System.Security.AccessControl namespace. The types used to get
user/group information are available in System.Security.Principal.
Security Basics
Before we get into the .NET implementation it is important to clarify a few
security-related concepts.
Object Rights
Each securable object has a list of operations that it supports. This list
of operations are known as security rights (or just rights). Rights are
generally represented as a set of bit flags internally. Rights can be
combined to form more complex rights. For example Full Control is a
combination of all rights. Certain rights are shared by all objects
including read and modify.
Due to the variety of rights available, security rights are not exposed directly
by the base security classes. Instead each object-dependent implementation
exposes its own set of rights as an enumeration. We will discuss these
rights later.
Identity References
In Windows both users and groups (known as identities) are represented as
globally unique values known as SIDs (security identifiers). SIDs are
well-formed but not easily remembered. All identities are referenced via
SIDs internally. Part of a SID includes the domain or machine that owns
the identity. All SIDs from the same machine/domain share at least a
partial identifier.
In .NET the SecurityIdentifier class is used to wrap a SID. While
useful for uniquely identifying a user or group creating an instance of this
class directly is rarely done. Instead this class is generally returned by
security-related methods.
Since working with SIDs is not generally useful, other than for uniqueness, .NET
provides a more user-friendly version available called NTAccount.
This class is specifically used for users and groups. It provides the
user-friendly name for a SID. You can easily convert between NTAccount
and SecurityIdentifier using the Translate method. In the few cases where NTAccount
can not map the SID to a friendly name it will simply use the SID. The
following example displays the name and SID of the current user.
WindowsIdentity.GetCurrent();
string name = id.User.Translate(typeof(NTAccount)).Value;
string sid = id.User.Translate(typeof(SecurityIdentifier)).Value;
Console.WriteLine(String.Format("Current User = {0}, [{1}]", name, sid));
Both NTAccount and SecurityIdentifier derive from
IdentityReference. The base class is generally used in the various
security calls to allow either class to be used. You will almost always want to use NTAccount.
DACLs and SACLs
In Windows there are discretionary access control lists (DACLs) and system access
control lists (SACLs). DACLs specify the rights assigned to identities on
an object. Each entry in the DACL is known as a ACE. SACLs determine
the auditing done on an object. In most cases you will be working with
DACLs.
In .NET DACLs are known as access rules and SACLs are known as audit rules.
Each access rule contains a security right and the identity that has (or does
not have) the right. .NET uses the abstract class AccessRule to
represent an access rule. Each audit rule contains a security right and
the condition under which it is logged (success or failure). .NET uses the
abstract class AuditRule to represent an audit rule. Neither of
these classes are used in most cases as the derived classes provide more
information.
In Windows an access rule can either allow or deny a specific right to an
identity. Most rules allow the right. In the rare case of a deny
right it takes precedence over all access rules. In fact access rules are
always listed such that deny rules come first. A deny rule always
overrides an allow rule. Therefore Windows (and your code) can stop
looking for a right as soon as it sees a deny rule for the right.
Object Security
Each securable object has a set of security properties exposed through an
ObjectSecurity-derived class. The derived class exposes methods to
access all the security settings of an object including the access and audit
rules and the owner. It is also through this class that we can modify the
security of an object. Unfortunately the base ObjectSecurity class
does not expose any of these methods. This makes working with securable objects
in a generic manner difficult. Instead the base class exposes properties that
define the types used to represent the access and audit rules, discussed later.
The following table defines some common securable objects and their associated object security
type.
| Object Type |
ObjectSecurity Class |
Accessor Class |
| Active Directory |
ActiveDirectorySecurity |
DirectoryEntry |
| Directory |
DirectorySecurity |
Directory, DirectoryInfo |
| File |
FileSecurity |
File, FileInfo |
| Registry Key |
RegistrySecurity |
RegistryKey |
Fortunately all the built in classes expose the same set of methods so, other
than the type name, working with each type is the same. There is another
solution to this delimma. Most of the security classes derive from
CommonObjectSecurity (which itself derives from ObjectSecurity).
This base class exposes the core methods that we will discuss later.
Therefore you can use CommonObjectSecurity in cases where you might want
to work with different object types. Remember that not all security
classes derive from this base class. ActiveDirectorySecurity is one
such case.
So how do you get
the object's security to begin with? As with the security classes, there
is no generic way to get this information. In the above table the Accessor
Class column identifies one or more classes that can be used to get access to an
object's security. In all cases except Active Directory, the type(s)
expose a GetAccessControl method. Static classes return the object
security for the parameter passed to the method. Instance classes return
the object security for the current instance.
The following example gets the security for the C:\Windows
directory.
DirectorySecurity sec = Directory.GetAccessControl(@"c:\Windows");
Access Rules
Each access rule
represents: a right, allow/deny flag and the associated identity. For the standard objects a custom enumerated type is defined to identify the
security rights available. The type of the enumeration is available through
the AccessRightType property on the object's security class, if
desired.
The following table defines the enumeration for the standard object types.
We will discuss the last two columns later.
| Object Type |
AccessRightType |
AccessRuleType |
AuditRuleType |
| Active Directory |
ActiveDirectoryRights |
ActiveDirectoryAccessRule |
ActiveDirectoryAuditRule |
| Directory |
FileSystemRights |
FileSystemAccessRule |
FileSystemAuditRule |
| File |
FileSystemRights |
FileSystemAccessRule |
FileSystemAuditRule |
| Registry Key |
RegistryRights |
RegistryAccessRule |
RegistryAuditRule |
Starting to notice a pattern yet. The access right enumerations all end in
-Rights. All the enumerations are marked as flags because several
of the rights are combinations of other rights (such as full control). To get the access
rules associated with an object use the GetAccessRules method. The
following example gets the access rules for a directory.
DirectorySecurity sec = Directory.GetAccessControl(@"c:\Windows");
foreach(FileSystemAccessRule rule in sec.GetAccessRules(true, true,
typeof(NTAccount)))
{
};
The GetAccessRules method accepts three parameters: include explicit,
include inherited and type of identity. The first parameter specifies whether
rights explicitly assigned to an object are returned. This is almost always the
desired case. The second parameter specifies where rights inherited from the
object's parent are returned. The final parameter determines the type of
identity reference to be returned. As discussed earlier there are two
standard types: NTAccount and SecurityIdentifier. For user
interfaces the NTAccount is the general choice.
Once we have the access rules we can enumerate them. The following table
lists the important properties of each rule.
| Property |
Description |
| AccessControlType |
Determines if this is an allowed or denied right |
| IdentityReference |
The user/group with the right |
| InheritanceFlags |
Controls how the right is inherited by children |
| IsInherited |
Determines if this is an explicit or inherited right |
| PropagationFlags |
Determines how the right propagates to children |
Notice that the actual rights are not part of the access rule, at least not
directly. Remember that CommonObjectSecurity provides a generic
implementation of the security. However the actual rights are enumerations
defined for each object type. Since CommonObjectSecurity has no way
to know what the enumerated values are it doesn't expose them as a strongly
typed property. The AccessMask property can be used to get the
underlying bitmask. Fortunately each AccessRule-derived class
exposes the rights as a strongly typed property named after the enumerated type.
This is the preferred method for getting the rights.
The following code will list all the rights associated with an object along with
some other property values.
Console.WriteLine(String.Format("{0,-30} Allowed Inherited {1,-15}",
"User/Group", "Right"));
Console.WriteLine(new string('-', 70));
DirectorySecurity sec = Directory.GetAccessControl(@"c:\Windows");
foreach
(FileSystemAccessRule rule in sec.GetAccessRules(true, true, typeof(NTAccount)))
{
Console.WriteLine(String.Format("{0,-30} {2} {3} {1,-15:g}",
rule.IdentityReference, rule.FileSystemRights, rule.AccessControlType ==
AccessControlType.Allow, rule.IsInherited));
};
There are a couple of important points about enumerating access rules. Firstly the
rights are not always a valid combination of flags from the enumeration.
Secondly an identity can appear more than once in the list. This can
occur for a variety of reasons, inheritance being one of the more common.
Therefore if you want to get all the rights owned by an identity you need to
enumerate all rules. Finally remember that there are both allow and deny
rules. Deny rules come before allow rules and take precedence.
The following method is a simple implementation for getting the rights of a
specific identity. It takes the associated group memberships into account and
deny rights.
static FileSystemRights GetObjectRights ( DirectorySecurity security,
WindowsIdentity id )
{
FileSystemRights allowedRights = 0;
FileSystemRights
deniedRights = 0;
foreach (FileSystemAccessRule rule in
security.GetAccessRules(true, true, id.User.GetType()))
{
//If the identity
associated with the rule matches the user or any of their groups
if
(rule.IdentityReference.Equals(id) ||
id.Groups.Contains(rule.IdentityReference))
{
//Filter out the generic rights so we get a nice
enumerated value
if (rule.AccessControlType == AccessControlType.Allow)
allowedRights |= (FileSystemRights)((uint)rule.FileSystemRights & 0x00FFFFFF);
else
deniedRights |= (FileSystemRights)((uint)rule.FileSystemRights &
0x00FFFFFF);
};
};
return allowedRights ^ deniedRights;
}
The method basically enumerates the access rules of the object and builds up the
list of rights for the user (taking their group membership into account).
Denied rights are tracked separately. After all the rights are determined the
allowed rights are returned with any denied rights removed. Notice the
filtering that is going on when adding the rights to the list. The filter
removes the extra bits and generic rights that might be associated with a rule.
These are not supported by the various Rights enumerations and would cause us
problems if we wanted to see the string representation.
In the Advanced Security Settings of a folder in Windows Explorer the Permission
entries map to these access rules. You will find that Explorer collapses
some of the rules for convenience. We will discuss the Apply To column
later.
Inheritance and Propagation
You can determine if a rule is inherited through the IsInherited property.
Whether a rule is inherited or not is determined by the InheritanceFlags
and PropagationFlags properties. The inheritance flags determine
who inherits the rule: child containers (folders), objects (files), both or
neither. The propagation flags determine whether the object (for which you
are adding the rule) gets the rule as well. The following table defines
the various combinations of flags and how they map to folder security in
Explorer.
| PropagationFlags |
InheritanceFlags |
Description (Explorer) |
| None |
ContainerInherit |
This folder and subfolders |
| |
ObjectInherit |
This folder and files |
| |
ContainerInherit | ObjectInherit |
This folder, subfolders and files |
| InheritOnly |
ContainerInherit |
Subfolders only |
| |
ObjectInherit |
Files only |
| |
ContainerInherit | ObjectInherit |
Subfolders and files only |
The table left out the propagation flag NoPropagationInherit. This
odd flag can be combined with any of the other entries in the table. When
applied it identifies the rule as applying only to the objects and containers
within the target object. In Explorer this maps to the checkbox below the
permission entries (when editing) that says Apply these permissions to
objects and/or containers within this container only.
Modifying Access Rules
Modifying access rules can be easy or hard depending on the situation. To give an identity a new right you create a
new access rule and then add it to the object security instance using the
AddAccessRule method. The following example gives the current user the
delete right to a directory.
WindowsIdentity id = WindowsIdentity.GetCurrent();
DirectorySecurity sec =
Directory.GetAccessControl(@"C:\Temp");
FileSystemAccessRule rule = new
FileSystemAccessRule(id.User, FileSystemRights.Delete, AccessControlType.Allow);
sec.AddAccessRule(rule);
Directory.SetAccessControl(@"c:\temp", sec);
Notice the call to SetAccessControl. The object security instance
you obtain from GetAccessControl is a snapshot of the current state of
the object. Changes you make to it do not actually get applied until you
call SetAccessControl. As a result it is when you call
SetAccessControl that you are most likely to get an exception such as for
unauthorized access or for a missing object. Whenever you make any changes
to an object's security you must remember to call this method to persist the
changes. In general you will make all the necessary changes before
attempting to persist them.
Removing rights is even easier. When
removing rights you have a little more flexibility. You can remove a
specific identity/right rule, all rights for a particular identity or all rights
for all users. To remove a specific identity/right use the
RemoveAccessRule. The following example removes the right we added
earlier.
WindowsIdentity id = WindowsIdentity.GetCurrent();
DirectorySecurity sec =
Directory.GetAccessControl(@"C:\Temp");
FileSystemAccessRule rule = new
FileSystemAccessRule(id.User, FileSystemRights.Delete, AccessControlType.Allow);
sec.RemoveAccessRule(rule);
Directory.SetAccessControl(@"c:\temp", sec);
To remove all the rights a user has to an object use PurgeAccessRules
instead. It simply requires the identity reference. You could also
the ModifyAccessRule method if you like. The various methods all
basically do the same thing. In fact methods implemented in derived
classes generally call into the base class methods. Prefer the versions
defined in derived classes but do not worry about going out of your way to call
them if the base classes are sufficient.
Modifying Inherited Rules
You can not just modify an inherited rule. The rule is defined and owned by
a parent object. To modify such a rule you first need to break the
inheritance. When breaking inheritance you have the option of either
copying the existing inherited rule or removing it altogether. In Explorer
when you attempt to edit an inherited rule you will notice that it does not
allow you to edit the inherited rights. You can, of course, add new
rights.
Unchecking the Include inheritable permissions from this
object's parent box will display a dialog prompting you to copy or
remove the inherited rules. In .NET you use the SetAccessRuleProtection
method on the security object. The following code demonstrates this
method.
DirectorySecurity sec =
Directory.GetAccessControl(@"c:\temp");
//Copy existing inherited rules
sec.SetAccessRuleProtection(false, true);
//or, remove inherited rules
sec.SetAccessRuleProtection(false, false);
After calling SetAccessRuleProtection you can then modifying all the
access rules on the object.
Audit rules
Audit rules work similar to access rules. In fact the only real
difference is that the members contain -Audit rather than -Access.
Therefore if you want to add a new audit rule use AddAuditRule. To
get the audit rules use GetAuditRules, etc.
Audit rules have the same properties as well except in lieu of
AccessControlType (to define allow and deny rules) they have the
AuditFlags property. Auditing can occur when the operation succeeds,
fails or both. The property is a bitwise flag combination of AuditFlags.
The following table defines the flags.
| AuditFlags |
Description |
| None |
No auditing |
| Success |
Audit on successful attempts |
| Failure |
Audit on failure attempts |
Ownership
Each securable object is owned by an identity. Owning an object gives an
identity the special privilege of changing the permissions irrelevant of access
rules. This prevents an owner from being locked out of their own object.
To get the owner of an object use the GetOwner method. The following
example gets the owner of the folder.
DirectorySecurity sec =
Directory.GetAccessControl(@"c:\temp");
IdentityReference owner = sec.GetOwner(typeof(NTAccount));
Setting the owner is just as easy using the SetOwner method. To take
ownership of an object requires the right permissions so this method can fail.
The following code makes the Administrators group the owner of the folder.
DirectorySecurity sec =
Directory.GetAccessControl(@"c:\temp");
NTAccount newOwner = new NTAccount("Administrators");
sec.SetOwner(newOwner);
Summary
There are quite a few features of the .NET access control that we did not cover.
However this article should hopefully get you started in the right direction.
|