Click here to Skip to main content
15,867,453 members
Articles / Programming Languages / C++
Article

The Windows Access Control Model Part 4

Rate me:
Please Sign up or sign in to vote.
4.86/5 (29 votes)
7 Sep 200543 min read 227.3K   6.8K   100   41
The final article in the access control series presents a guide to the access control editor and its associated ISecurityInformation interface.

Filepermsbox. A program to view the security descriptor of a file

Figure 40: Filepermsbox, an ACL editor for directories and files.

About the series

This 4 part series will discuss the Windows access control model and its implementation in Windows NT and 2000.

Part 4 is the final part of the access control series. This was going to be the only article I was going to post on the topic, but you needed the introductory topics as a prerequisite.

In part 4, I will show you how to implement the ACL editor in your own application, so you can secure anything. I will start off by describing the ACL editor and its features (with the help of some screenshots). I will describe how to utilise the ACL editor for your own objects. Then, the real work begins: the ISecurityInformation interface will be explained and implemented. Each method of the ISecurityInformation will be explained, and example implementations will be presented. A separate section will be devoted to the ISecurityInformation::SetSecurity method (since it is so complicated). Once that is done, we will briefly discuss the other interfaces you can implement (ISecurityInformation2, ISecurityTypeInfo, and IEffectivePermission).

The ACL editor is only available for Windows 2000 or higher.

Windows NT3 and 4 included an older ACL editor. In order to display the older ACL editor, you'll have to call functions not documented on MSDN (you'll need to go to the Sysinternals [^] website for that). If you are using .NET, Keith Brown has a reusable class library [^] that will allow you to use the ACL editor (the associated article has some cool information on the ACL editor too).

Unfortunately, I won't explain how to use the ACL editor as a good security descriptor, or help you regain access to that locked folder of yours. If you need to unlock access to a file you should go to KB308421 [^].

The sample program, Filepermsbox, is a property sheet extension for Windows XP home edition (though it works on any OS above Windows 2000). It recreates the full security tab even if simple file and folder sharing was enabled. That means you Home users no longer need to reboot in safe mode to change file security descriptors. However, Filepermsbox could also be useful for third party shells and security utilities. You can access the security tab in one of two ways. The first way is to use the front-end application (fig. 40). Simply choose your file, and the ACL editor will appear so you can edit it. If you register the DLL (either by the setup or through regsvr32), you can access Filepermsbox though the shell extension. Select a file, open its properties, and select the Filepermsbox tab.

Source, binaries and an installer are provided.

Table of Contents

The table of contents is for the entire series.

  1. Part 1 - Background and Core Concepts. The Access Control Structures
    1. The Security Identifier (SID)
    2. The Security Descriptor (SD)
    3. The Access Control List (ACL)
    4. Choosing a good discretionary access control list
    5. Windows 2000 Inheritance Model
    6. The Token
    7. A Note on the Security Descriptor Definition Language
    8. Coming Up
  2. Part 2 - Basic Access Control programming
    1. Choosing your language
    2. Fun with SIDs
    3. Which Groups are you a member of?
    4. Enabling Token Privileges
    5. Dissecting the Security Descriptor
    6. Walking an Access Control List
    7. Do I Have Access?
    8. Creating an Access Control List
    9. Creating and Editing a Secure Object
    10. Making Your Own Classes Secure
    11. Toy Programs Download
  3. Part 3 - Access Control programming with .NET v2.0
    1. A history of .NET security.
    2. Reading a SID in .NET.
    3. Whoami .NET.
    4. Privilege handling in .NET.
    5. Running as an unprivileged user in .NET.
    6. Obtaining and editing a security descriptor in .NET, and applying it to an object.
    7. Access checks in .NET.
    8. NetAccessControl program and AccessToken class library.
  4. Part 4 - The Windows 2000-style Access Control editor
    1. Features of the ACL editor (ACLUI)
    2. Getting Started
    3. Implementing the ISecurityInformation Interface
    4. ISecurityInformation::SetSecurity
    5. Optional Interfaces
    6. Presenting the interface
    7. Filepermsbox - A program to display the security descriptor on an NTFS file or folder
    8. History

28. Features of the ACL Editor (ACLUI)

If you ran the sample program in part 3 [^], you may have come across the ReadSD program that can read and edit a registry security descriptor. Apart from the nondescript user interface, you may have noticed many problems with my ACL editor.

  • Access masks appear as an untranslated hexadecimal number. Ideally, the access mask should be presented in textual form (such as "Read", "Write", or "All Access").
  • Auto-inherited ACEs are presented separately. As a result, most security descriptors will appear empty, confusing the user. We need to find a way to show both explicit ACEs and inherited ACEs at the same time.
  • The DataGridView performs no validation of the username nor the access mask until it is too late.
  • The Inheritance flags editor is presented as a modal dialog. You cannot edit anything else whilst setting the flags.
  • If you aren't allowed to edit the registry security (quite common when you run as a limited user), ReadSD could fail.
  • Most options are too confusing to inexperienced users. This increases the chances of users making mistakes.
  • Last, but not least, ReadSD only works for registry keys.

Dialog 1: The General ACL Editor

The Windows 2000 ACL Editor first appeared as a redistributable for Windows NT4.0. It offers a common user interface that can read and edit a security descriptor and its parts, including the control flags, the DACL, the SACL, and the owner. It's called the ACL editor (it may also be called the security tab, the permissions sheet, or ACLUI). The term ACL editor is somewhat of a misnomer—the interface can edit the SACL and the owner of an object, as well as its DACL. More significantly, the ACL editor is object-agnostic. The same ACL editor can be used to change the security for a registry key, COM interface, DCOM class, file, directory, service, or process object.

Let's take a look at the interface. Select a file, and open up its properties. In the property sheet that follows, select the Security tab and now you can start viewing the ACL editor.

If you are on Windows XP, the Security tab will be unavailable by default. In that case you can either try to disable "simple file sharing" in Folder Options, or if that can't be done, download my Filepermsbox sample program.

An annotated ACL editor with its developer features

Figure 41: The main property sheet of the ACL editor.

The general ACL editor is a property sheet consisting of a list view (user name) and a checklist box (generic access rights). The first list is responsible for showing which users / groups / computers (they're collectively called principals, but for simplicity, I'll just call them users. Wait a moment... didn't I call them trustees before?) the DACL applies to. The bottom checklist displays the generic access rights for the selected user. Notice a number of features of the general property sheet:

  1. The title of the box displays the name of the object. The ACL editor asks you for the title of the object.
  2. The list of SIDs are obtained from the DACL, and the ACL editor may ask you to translate these SIDs to user names.
  3. There is an Advanced button to open up three more dialog boxes. Once again, the ACL editor asks you if you want to enable this button.
  4. If you want to add a user, you can click the "Add..." button. This will show the dialog box in fig. 42.
  5. You can make the ACL editor read only. The controls that handle ACL editing are grayed.
  6. The ACL editor shows the relevant access rights that apply to this object. It asks you for the list of access rights.
  7. Inherited ACEs and other allow / deny ACEs are merged into one user permission. The ACL editor bunches together deny DACEs and allow DACEs, asks you for generic access rights, and then remaps the rights so that they can be merged. In short, if two DACEs apply to the same user, they are appended.

IdsObjectPicker Dialog Box

Figure 42: Choosing a server / user

When you press Add..., you are presented with another dialog box (fig. 42). This window is the IDsObjectPicker dialog, which allows you to select users, groups or computers. For this dialog, the ACL editor asks you to supply the domain name where it can lookup the list of users. Unless you are editing a remote object, this will normally be the default domain name. Once again, the ACL editor will ask you to translate SIDs to user names when it fills the details view at the bottom of the dialog box.

If you are editing a DACL with entries too complicated to be represented as generic rights, the ACL editor will name the DACE "Special". In that case, you will need to enter the advanced ACL editor.

The Advanced ACL Editor

The advanced permissions tab of the ACL editor (with annotations)

Figure 43: The advanced ACL editor.

If it was enabled, you can press the Advanced button to receive the full power of the ACL editor. The advanced ACL editor is a huge dialog sketched in fig. 43. It consists of four tabs which edit the DACL, SACL, owner and (from WinXP) effective permissions. The first dialog you see is the advanced permissions dialog, which allows you to fully edit the DACL. Its central user interface is a details view that shows all the entries of the DACL. This time, the inherited ACEs and the explicit ACEs are shown separately.

For each ACE, there is an entry for the ACE type (Allow / Deny), the user name, the permission name, the propagation flags, and (from WinXP) the inheritance source. With the exception of the ACE type, all of the entries in the details view are object specific (e.g. List folder contents may make sense for a directory object, but it's meaningless for a mutex). To complete the encapsulation, the ACL editor will ask you for the names. When the advanced permissions box is initialized, you will be asked to translate the following:

  • SIDs to user names.
  • Generic rights to specific rights.
  • Access rights to text.
  • Propagation flags to text.
  • Inherited ACEs to inheritance source (where the ACE came from).

If you want, you can also have a "Default" button, that resets the DACL to its default. When the user presses "Default", the ACL editor will ask you to supply a default security descriptor. This assumes you know what the default security descriptor is (and before adding a default security descriptor, remember your definition of a default security descriptor may not be everyone else's definition).

If your object supports auto-inheritance, you can display two further checkboxes. The "Inherit from parent" checkbox toggles the auto-inheritance of the DACL. When you disable auto-inheritance, you are asked if you want to make the inherited ACEs explicit, or remove them (if you read part 3, this is equivalent to asking for the preserveInheritance member of ObjectSecurity.SetAccessRuleProtection).

The "replace permission entries" checkbox has been affectionately called the sledgehammer button by Keith Brown [^]. It's so called because it goes down to every child object and resets all of their security descriptors to a simple default one (e.g. it can unhide your "secret documents" folder).

Annotated version of the ACE Dialog box

Figure 44: The ACE dialog box allows you to edit the individual access control entries in the ACL.

When the user presses "Add...", or double clicks on an entry in the list, a new dialog box appears, similar to fig. 44 (I'll call it the ACE dialog box). The ACE dialog box allows the user to edit the properties of that ACE. They can edit / add rights to the ACCESS_MASK, control the propagation of access rights, and (for domain style objects) the object properties.

When rendering the window, the ACL editor will ask you for the following:

  • The textual form of each access right. (Reminder: access rights combine to form an access mask.)
  • A translation of AceFlags to text. (e.g. CI means "propagate permission to child registry keys").
  • The textual form of the GUID in ObjectType (domain ACEs only).
  • Translating of SIDs to user names (already implemented elsewhere).

Notice you can specify both access allowed and access denied rights in the same ACE. In reality, the ACL editor will split up the access allowed and access denied ACEs into separate entries. You will see the resultant ACL when you exit this box by pressing OK.

If your object doesn't support one of these features (e.g. files don't have children, so propagation doesn't apply to them), you can ask the ACL editor to remove some features.

The ACE dialog becomes interesting when you attempt to edit auto-inherited entries. From part 3, you will know that you cannot edit auto-inherited ACEs. To compensate for this, the ACE dialog box grays the auto-inherited access masks, showing the user that they cannot edit them. Instead, the user must click on Deny to remove auto-inherited entries. Thanks to this wonder UI design, the ACL editor has made the user override the auto-inherit ACEs with deny entries!

Other Features of the ACL Editor.

When the user presses Apply to commit the security descriptor, a number of background checks occur on the ACL. One of the checks that is performed is the ordering of access control entries. If the ACL editor determines that the ACL is not ordered correctly, it sorts the ACL (informing the user of what it did). Contrary to what the docs may say, it is possible to override this sort.

If you've enabled the "SeSecurityPrivilege", you will want to consider showing the Auditing tab. This enables the user to edit the SACL, much the same way as the DACL. The ACL editor will handle the differences between an SACL and a DACL; so as far as you're concerned, the auditing page is a clone of the DACL page. The third tab enables the user to change the owner of the security descriptor. Due to a bug in the Windows 2000 ACL editor, the user can only effectively take ownership of the file (or their group). They cannot relinquish ownership, or give someone else the ownership (this bug was fixed in Windows Server 2003, where any user can be selected). Unless you're on Windows 2000, there is a final tab called the "Effective Permissions" tab. This tab acts as a front-end for the GetEffectiveRightsFromAcl() API, and enables the user to see what rights they effectively have (after including group rights, deny entries, and auto-inherited ACEs).

In order to display the ACL editor, you will need to implement an interface called ISecurityInformation. If you want to implement that interface... you'll have to learn a whole new batch of AdvAPI library functions:

  • MapGenericMask()
  • BuildTrusteeWithSid()
  • GetInheritanceSource()
  • FreeInheritedFromArray()
  • GetEffectiveRightsFromAcl()
  • SetNamedSecurityInfo()
  • GetNamedSecurityInfo()
  • TreeResetNamedSecurityInfo()

29. Getting Started

Although there are samples (particularly in the Platform SDK) to get you started, and two MSDN Magazine articles [^] mentioning the editor, there are some very peculiar bugs with the ACLUI, not mentioned anywhere else. When you look at the ISecurityInformation interface, it may look like a COM object, but in reality, it isn't a COM object (fig. 45).

class CSecurityInformation: ISecurityInformation, IUnknown
{
public:
  /* IUnknown methods */
  STDMETHOD(QueryInterface)(REFIID, LPVOID *);
  STDMETHOD_(ULONG, AddRef)(void);
  STDMETHOD_(ULONG, Release)(void);

  /* ISecurityInformation methods */
  STDMETHOD(GetObjectInformation)(PSI_OBJECT_INFO pObjectInfo);
  STDMETHOD(GetSecurity)(SECURITY_INFORMATION si, \
    PSECURITY_DESCRIPTOR *ppSD, BOOL fDefault);
  STDMETHOD(SetSecurity)(SECURITY_INFORMATION si, \
    PSECURITY_DESCRIPTOR pSD);
  STDMETHOD(GetAccessRights)(const GUID* pguidObjectType, \
    DWORD dwFlags, PSI_ACCESS *ppAccess, ULONG *pcAccesses, \
    ULONG *piDefaultAccess);
  STDMETHOD(MapGeneric)(const GUID *pguidObjectType, \
    UCHAR *pAceFlags, ACCESS_MASK *pmask);
  STDMETHOD(GetInheritTypes)(PSI_INHERIT_TYPE *ppInheritTypes, \
    ULONG *pcInheritTypes);
  STDMETHOD(PropertySheetPageCallback)(HWND hwnd, UINT uMsg, \
    SI_PAGE_TYPE uPage);

  /* Your custom methods, constructor, destructor */
private:
  ...
};

Figure 45: A sample blueprint to implement the ISecurityInformation interface.

That's a pretty intimidating list of functions to implement, isn't it? (It rivals DirectX interfaces in terms of complexity.) What's worse, there is no Typelib to help you implement this interface (so neither ATL nor .NET will help you here). You're either going to have to implement these "COM" functions the low level way [^], or get a library [^] that does it for you.

Constructors / Destructors

If you look closely, you'll see that none of the ISecurityInformation methods take in a filename. Remember, the ACL editor doesn't only work for files. It works for registry keys, kernel objects, services, or anything you like. And here's why. The name and type of object are managed by you, not the ACL editor. So for the above class, I'd recommend you add a class member that denotes the name of your object (or objects, as will be explained later).

This is a pretty good place to enable any privileges you may need. Some of the privileges you may need are "SeSecurityPrivilege", "SeBackupPrivilege", "SeRestorePrivilege", "SeTakeOwnershipPrivilege" and "SeChangeNotifyPrivilege".

If you maintain a user interface, I'd also recommend you have a member denoting the owner window of the ACL editor.

IUnknown

Since this object implements IUnknown, you are required to maintain a reference count (starting at 1). For QueryInterface, the interfaces implemented are IUnknown and ISecurityInformation (and if you want, ISecurityInformation2, ISecurityObjectTypeInfo and IEffectivePermission). Due to an undocumented bug in ACLUI, your object must support the single threaded apartment model.

30. Implementing the ISecurityInformation interface

Now that you have implemented a constructor, destructor, and IUnknown, it's time to add the real functionality into your application.

1. ISecurityInformation::GetObjectInformation() and SI_OBJECT_INFO

When this method is called, you are required to fill up an SI_OBJECT_INFO struct (which influences the behaviour of the ACL editor) and return it to the caller. The SI_OBJECT_INFO struct is mostly self explanatory (with some optional members), but one member is worthy of discussion:

  • dwFlags: A set of flags that influences the behaviour of the ACL editor. For example, if you do not want the user pressing "Advanced" to show the advanced ACL editor, you can disable it here. If you don't want the user editing the security descriptor, set the SI_READONLY flag. Here are the more important flags:
    • SI_ADVANCED: Specifies the "Advanced" button will be shown so that the user can enter the advanced UI.
    • SI_CONTAINER: Indicates that your object is a directory (or behaves like a directory). You should set this flag if the object is a directory so the ACL editor can apply inheritance on the object. You may also want to enable the inheritance stuff.
    • SI_EDIT_AUDITS: Shows the "Auditing" tab (so the user can edit the SACL). To view / edit the SACL, you must have the "SeSecurityPrivilege".
    • SI_EDIT_PERMS: Shows the "Advanced" permissions tab (so the user can fully edit the DACL).
    • SI_EDIT_OWNER: Shows the Owner tab (so the user can change or take ownership).
    • SI_EDIT_PROPERTIES: Shows the properties tab (so the user can edit the properties associated with the object).
    • SI_EDIT_EFFECTIVE (undocumented, for Windows XP and later): Shows the "Effective Permissions" tab.
    • SI_READONLY: Does not allow the user to change the security descriptor.
    • SI_OWNER_READONLY: Does not allow the user to change the owner (to change ownership, the user must be the owner, or have the "SeTakeOwnershipPrivilege").
    • SI_MAY_WRITE (undocumented): "Not sure if the user may write permission".
    • SI_NO_ADDITIONAL_PERMISSION (Undocumented): ??????
    • SI_NO_ACL_PROTECT: Select this if you haven't implemented inheritance. This hides the auto-inherit checkbox.
    • SI_OWNER_RECURSE: Shows the "Sledgehammer" button, which enables the user to reset all children's security descriptors to default. Do not show this unless you have implemented recursive walking in your object.
    • SI_RESET_DACL_TREE: Like SI_OWNER_RECURSE, but for the Advanced Permissions tab.
    • SI_RESET_SACL_TREE: Like SI_RESET_DACL_TREE, but for the "Auditing" tab.
    • SI_SERVER_IS_DC: Set this if you have determined the computer is in a domain environment.
    • SI_EDIT_ALL: shorthand for SI_EDIT_AUDITS | SI_EDIT_PERMS | SI_EDIT_OWNER.
    • SI_NO_TREE_APPLY: Hides the "Apply these permissions to objects and/or containers within this container only" checkbox. Set this flag if your object does not support inheritance.
    • SI_PAGE_TITLE: Indicates you want a custom title to appear in the ACL editor. Specify the alternate title in the pszPageTitle member.
    • SI_RESET: Shows the "Default" button, which will reset the security descriptor to defaults. If you don't know what the default security descriptor is, do not enable this flag.
    • SI_RESET_SACL, SI_RESET_DACL, SI_RESET_OWNER: shows the "Default" button for certain tabs only.
    • SI_OBJECT_GUID: (domain-specific) If your object supports multiple inheritance, you should enable this flag.

My advice on choosing a suitable dwFlags: First disable features your object doesn't support (kernel objects don't support auto-inheritance, so you can remove most of the container stuff). Next disable features you are denied from doing (if you don't have the "SeSecurityPrivilege", don't show the Auditing tab). Then disable features that you feel will be too complex to implement. For example, if you don't want to implement the complicated sledgehammer button (more on this in section 33), you can disable the SI_RESET_DACL_TREE. The Filepermsbox sample program has two predetermined dwFlags (one for file, one for folder), then programmatically disable flags based on what it's allowed to do.

Warning: the strings and handles passed to ACLUI must remain valid throughout the lifetime of the editor, and only in the destructor do you get a chance to free any resources. If you use dynamically allocated memory without some kind of Copy-On-Write optimization, start watching your app leak memory.

STDMETHODIMP CSecurityInformation::GetObjectInformation
  (PSI_OBJECT_INFO pObjectInfo)
{
  pObjectInfo->dwFlags = SI_ADVANCED | SI_EDIT_ALL | SI_EDIT_EFFECTIVE;
  pObjectInfo->hInstance = GetModuleHandle(NULL);
  pObjectInfo->pszServerName = NULL;
  pObjectInfo->pszObjectName = this->FileName;
  return S_OK;
}

Figure 46: Sample <SMALL>GetObjectInformation</SMALL> implementation.

2. ISecurityInformation::GetAccessRights() and SI_ACCESS[ ]

When we toured the ACL editor, I described how the ACLUI asks your class for a string each time it fills out an access right. This is the function ACLUI uses. Rather than calling GetAccessRights() for each access right, ACLUI will call this function once. What you do is supply a hash table (implemented as a SI_ACCESS array, C-style), and the ACL editor will use this hash table to map access masks into a display string and their values.

Access rights are object specific (e.g. a process object would have a "Create Process" access right, rather than a "Read File" right), but as a guide to what you should supply, you can check out Filepermsbox's SI_ACCESS structure in fig. 47. The SI_ACCESS structure consists of the following members:

  • pguid: If you are securing a domain style object, specify the object GUID here. Otherwise, set this to GUID_NULL.
  • mask: the value assigned to the action (e.g. for a file read, this would be FILE_GENERIC_READ).
  • pszName: The display name of the action (e.g. "Read").
  • dwFlags: Where the permission is displayed (in the main sheet, the advanced sheet, show for folders or files, ...).
    • SI_ACCESS_GENERAL: The entry will be shown on the Basic security page (use for generic access rights like "Modify", "Read & Execute").
    • SI_ACCESS_SPECIFIC: The entry will be shown on the Advanced security page (use for advanced access rights like "Append Data", "Read Extended Attributes").
    • SI_ACCESS_PROPERTIES: The entry will be shown on the Properties page (use for property changing rights).
    • 0 (zero): If this entry is encountered in the ACL, it will be labeled as such in the Advanced Security page (e.g. "Read, Write and Execute").
    • SI_ACCESS_CONTAINER: The entry will only be shown if the object is a container (behaves like a directory).
    • CONTAINER_INHERIT_ACE: The CI flag is set in the ACL.
    • OBJECT_INHERIT_ACE: The OI flag is set in the ACL.
    • INHERIT_ONLY_ACE: The IO flag is set in the ACL.
const SI_ACCESS g_siObjAccesses[] =
{
  {&GUID_NULL, FILE_ALL_ACCESS, L"Full Control", SI_ACCESS_GENERAL |
    SI_ACCESS_SPECIFIC | CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE},
  {&GUID_NULL, FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE |
    DELETE, L"Modify", SI_ACCESS_GENERAL | CONTAINER_INHERIT_ACE |
    OBJECT_INHERIT_ACE},
  {&GUID_NULL, FILE_GENERIC_EXECUTE, L"Execute", SI_ACCESS_GENERAL |
    CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE},
  {&GUID_NULL, FILE_GENERIC_READ | FILE_GENERIC_EXECUTE, L"List Folder Contents",
    SI_ACCESS_CONTAINER | CONTAINER_INHERIT_ACE},
  {&GUID_NULL, FILE_GENERIC_READ, L"Read", SI_ACCESS_GENERAL |
    CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE},
  {&GUID_NULL, FILE_GENERIC_WRITE, L"Write", SI_ACCESS_GENERAL |
    CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE},

  {&GUID_NULL, FILE_EXECUTE, L"Traverse Folder/Execute File",
    SI_ACCESS_SPECIFIC},
  {&GUID_NULL, FILE_READ_DATA, L"List Folder/Read Data",
    SI_ACCESS_SPECIFIC},
  {&GUID_NULL, FILE_READ_ATTRIBUTES, L"Read Attributes",
    SI_ACCESS_SPECIFIC},
  {&GUID_NULL, FILE_READ_EA, L"Read Extended Attributes", SI_ACCESS_SPECIFIC},
  {&GUID_NULL, FILE_WRITE_DATA, L"Create Files/Write Data", SI_ACCESS_SPECIFIC},
  {&GUID_NULL, FILE_APPEND_DATA, L"Create Folders/Append Data",
    SI_ACCESS_SPECIFIC},
  {&GUID_NULL, FILE_WRITE_ATTRIBUTES, L"Write Attributes",
    SI_ACCESS_SPECIFIC},
  {&GUID_NULL, FILE_WRITE_EA, L"Write Extended Attributes",
    SI_ACCESS_SPECIFIC},
  {&GUID_NULL, FILE_DELETE_CHILD, L"Delete Children", SI_ACCESS_SPECIFIC},
  {&GUID_NULL, DELETE, L"Delete", SI_ACCESS_SPECIFIC},
  {&GUID_NULL, READ_CONTROL, L"Read Permissions", SI_ACCESS_SPECIFIC},
  {&GUID_NULL, WRITE_DAC, L"Set Permissions", SI_ACCESS_SPECIFIC},
  {&GUID_NULL, WRITE_OWNER, L"Take Ownership", SI_ACCESS_SPECIFIC},
  {&GUID_NULL, SYNCHRONIZE, L"Synchronize", SI_ACCESS_SPECIFIC},
  {&GUID_NULL, FILE_GENERIC_EXECUTE, L"Traverse/Execute", 0},
  {&GUID_NULL, FILE_GENERIC_EXECUTE | FILE_GENERIC_WRITE,
    L"Write and Execute", 0},
  {&GUID_NULL, FILE_GENERIC_EXECUTE | FILE_GENERIC_WRITE |
    FILE_GENERIC_READ, L"Read Write and Execute", 0},

  { &GUID_NULL, 0, L"None", 0 }
};

STDMETHODIMP CSecurityInformation::GetAccessRights
  (const GUID*, DWORD, PSI_ACCESS *ppAccesses,
  ULONG *pcAccesses, ULONG *piDefaultAccess)
{
  *ppAccesses = const_cast<SI_ACCESS *>(g_siObjAccesses);
  *pcAccesses = sizeof(g_siObjAccesses) /
    sizeof(g_siObjAccesses[0]);
  *piDefaultAccess = 2;
  return S_OK;
}

Figure 47: A sample <SMALL>GetAccessRights</SMALL> implementation, for files and folders.

If you've set the dwFlags parameters properly, you can use just the one array of SI_ACCESSes to send to the ACL editor. However, if you need different lists depending on whether the advanced page or the basic page is being shown, GetAccessRights() will tell you which page is being initialized. Don't forget to set the number of entries and the default access right before returning to the caller. If the ACL editor comes across an access right that cannot be mapped into one of these entries, it will say the object has "Special permissions". One wrong entry in the SI_ACCESSes array could mean the difference between having all "Special permissions", or the correct permission name.

The SI_ACCESS array is the first of a number of C-style hash tables you need to supply to ACLUI.

3. ISecurityInformation::MapGeneric() and GENERIC_MAPPING

Apart from object abstraction, this is one case where the ACLUI sets itself apart from my ReadSD program [^]. This method serves as a bridge for MapGenericMask(). The ACL editor has supplied you with an ACCESS_MASK, and is expecting you to remap generic rights to object specific rights using MapGenericMask(). The GENERIC_MAPPING structure (another hash table) is object specific, and may also be used elsewhere in the class, so Microsoft recommends you have the GENERIC_MAPPING as a global structure (don't worry, you can keep it constant). You may already know how to map generic rights into object specific rights if you bothered to look inside ReadSD [^], but in case you haven't, there are two tasks to perform here:

  1. Fill out a GENERIC_MAPPING structure which will transform the system generic access rights into specific rights.
  2. Call MapGenericMask() so the ACCESS_MASK is cleared of generic rights.

If your object supports GUIDs, you can build separate GENERIC_MAPPINGs depending on the GUID supplied as necessary. For files / folders, the GENERIC_MAPPING is relatively simple.

GENERIC_MAPPING g_ObjMap =
{
  FILE_GENERIC_READ,
  FILE_GENERIC_WRITE,
  FILE_GENERIC_EXECUTE,
  FILE_ALL_ACCESS
};

STDMETHODIMP CSecurityInformation::MapGeneric
  (const GUID*, UCHAR *, ACCESS_MASK *pmask)
{
  MapGenericMask(pmask, &g_ObjMap);
  return S_OK;
}

Figure 48: Bridging MapGeneric() to MapGenericMask().

4. GetInheritTypes() and SI_INHERIT_TYPE[ ]

This method is responsible for translating the cryptic CONTAINER_INHERIT_ACE, etc. into a more meaningful entry (e.g. "This folder, and child folders"). This also includes combination names like CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE (which should translate to "This folder, child folders and files"). The process involves filling out an array of SI_INHERIT_TYPEs (the ACL editor sure does use a lot of C-style hash tables). Think of GetInheritTypes() as a GetAccessRights() for propagation flags.

const SI_INHERIT_TYPE g_InheritTypes[] =
{
  /* Change these strings as necessary */
  &GUID_NULL, 0, L"This Object",
  &GUID_NULL, CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE,
    L"This object, inherited objects and containers",
  &GUID_NULL, CONTAINER_INHERIT_ACE,
    L"This object and containers",
  &GUID_NULL, OBJECT_INHERIT_ACE,
    L"This object and inherited objects",
  &GUID_NULL, INHERIT_ONLY_ACE | CONTAINER_INHERIT_ACE |
    OBJECT_INHERIT_ACE, L"Inherited containers/objects",
  &GUID_NULL, INHERIT_ONLY_ACE | CONTAINER_INHERIT_ACE,
    L"Inherited Containers",
  &GUID_NULL, INHERIT_ONLY_ACE | OBJECT_INHERIT_ACE,
    L"Inherited Objects"
};


STDMETHODIMP CSecurityInformation::GetInheritTypes
  (PSI_INHERIT_TYPE *ppInheritTypes, ULONG *pcInheritTypes)
{
  *ppInheritTypes = const_cast<SI_INHERIT_TYPE *>(g_InheritTypes);
  *pcInheritTypes = sizeof(g_InheritTypes) /
    sizeof(g_InheritTypes[0]);
  return S_OK;
}

Figure 49: Translating AceFlags to text using GetInheritTypes().

5. ISecurityInformation::PropertySheetPageCallback()

This method notifies you whenever a new page is being displayed, or destroyed. This may be useful if you are subclassing the ACL editor, because it sends you the new window handles. I tend to use GetLastActivePopup() to get the dialog box window instead, but you may or may not need them. If you don't need the window handles, you can simply return S_OK.

STDMETHODIMP CSexurityInformation::PropertySheetPageCallback
  (HWND /* hwnd */, UINT /* uMsg */, SI_PAGE_TYPE /* uPage */)
{
  return S_OK;
}

Figure 50: Implementation of PropertySheetPageCallback().

6. ISecurityInformation::GetSecurity()

I've deliberately left the Get / SetSecurity() methods for last, because this is where the real fun begins. When the GetSecurity() method is called, you are required to fill a SECURITY_DESCRIPTOR variable with the requested SECURITY_INFORMATION.

At first sight this method appears to be an inviting filter for the Get*ObjectSecurity() APIs (after all there's a SECURITY_INFORMATION, and a SECURITY_DESCRIPTOR, and we have the object name). If you've read part 2, you'll know that you should not use these APIs. Reminder: these APIs do not support inheritance. So what do we use instead?

You should use GetSecurityInfo() or GetNamedSecurityInfo(). Since the ISecurityInformation::GetSecurity() calls were designed for Get*ObjectSecurity(), some translation to GetNamedSecurityInfo() is in order. The Get variants are the easier of the two to translate.

Had the user pressed the Default button, ACLUI will call GetSecurity() with fDefault set to true. Then you should set ppSecurityDescriptor to a default SECURITY_DESCRIPTOR (Filepermsbox calls out its helper function GetDefaultSecurity() to perform this task). When choosing a default security descriptor, make sure that the default is a secure one. I've given some guidelines on choosing a default security descriptor back in part 1.

Windows Setup gets the default security descriptors from the file "%windir%\security\templates\setup security.inf".

When you're designing a default security descriptor, note that what you define as a default security descriptor may not agree with the end user's definition of default security descriptor. If you're not sure what the default security descriptor should be, disable ACLUI's Default button feature (Windows disables it).

One more thing, before you return the security descriptor to the caller, it must point to a block of memory that was LocalAlloc()'ed. This is because the ACL Editor will LocalFree() it later. GetNamedSecurityInfo() calls LocalAlloc() to allocate the security descriptor automatically, so you can just pass the returned security descriptor to the ACL editor. Unfortunately, it's bad news if you are using something like .NET, because it means you have to marshal a native object outside of its scope.

STDMETHODIMP CSecurityInformation::GetSecurity
  (SECURITY_INFORMATION si, PSECURITY_DESCRIPTOR *ppSD,
  BOOL fDefault)
{
    *ppSD = NULL ;
    if (fDefault)
    {
        return E_NOTIMPL ;
    }

    SetLastError(ERROR_SUCCESS) ;
    GetNamedSecurityInfo(this->FileName,
      SE_FILE_OBJECT, si, NULL, NULL, NULL, NULL, ppSD) ;

    return S_OK;
}

Figure 51: a simple implementation of GetSecurity() (GetDefaultSecurity() left unimplemented).

ISecurityInformation::SetSecurity()

Once the user has changed the security descriptor, pressed Apply and responded to the warnings, this method is called. This is easily the most difficult of the methods to implement. Some of the problems may be self inflicted (Filepermsbox stores the filenames in an STL list, and needs to convert each name into a writable C-style string), some are due to ACLUI being object-agnostic, but most are due to ACLUI doing almost no work until this method.

You can find out which pieces the user altered by peering into the SECURITY_INFORMATION parameter. All the changes will be placed in a modification security descriptor. Your task is to apply the changes onto the object. Recover the name of your object and start enabling any privileges you need. Then take a deep breath, and good luck.

7. Set Security with SetSecurityInfo().

The docs [^] for SetSecurity() say: "The application must merge the new security descriptor parts into the object's existing security descriptor". The older security functions which replace the security descriptor parts rather than merge them are out of the question.

If you've read part 2, you'll know that using SetSecurityInfo() (or SetNamedSecurityInfo()) is the preferred method of writing a security descriptor. In order to use SetSecurityInfo(), you need to split up the SECURITY_DESCRIPTOR into its pieces. Getting the DACL, SACL, Group, and Owner isn't too difficult (especially with the help of GetSecurityDescriptorDacl() and friends).

STDMETHODIMP CSecurityInformation::SetSecurityForObject
  (TCHAR *ObjectName, SECURITY_INFORMATION psi, PSECURITY_DESCRIPTOR pSD)
{
  ...
  DWORD dwErr = 0;
  BOOL AclPresent = FALSE, AclDefaulted = FALSE;

  if(psi & DACL_SECURITY_INFORMATION)
  {/* Get the new DACL. */
    dwErr = GetSecurityDescriptorDacl(pSD, &AclPresent,
      &NewDAcl, &AclDefaulted);
  }
  if(psi & SACL_SECURITY_INFORMATION)
  {/* Get the new SACL. */
    dwErr = GetSecurityDescriptorSacl(pSD, &AclPresent,
      &NewSAcl, &AclDefaulted);
  }
  if(psi & GROUP_SECURITY_INFORMATION)
  {/* Get the New Group SID */
    dwErr = GetSecurityDescriptorGroup(pSD, &SidGroup, &AclDefaulted);
  }
  if(psi & OWNER_SECURITY_INFORMATION)
  {/* Get the New Owner SID */
    dwErr = GetSecurityDescriptorOwner(pSD, &SidOwner, &AclDefaulted);
  }

  /* You now have the owner, group, DACL, and SACL. */
  ...
}

Figure 52: extracting the security descriptor parts so you can call SetSecurityInfo().

The harder part is translating the control bits. In part 2 [^] (when setting the security descriptor), I presented a mapping that can transform the control bits into appropriate security information flags. This mapping will help you detect whether the user has checked the auto-inherit box, and translate any inheritance settings into appropriate SECURITY_INFORMATION flags.

...
/* Continued from Figure 52 */

SECURITY_DESCRIPTOR_CONTROL pSDControl = {0};
DWORD SDRevision = 0;
dwErr = GetSecurityDescriptorControl(pSD, &pSDControl, &SDRevision);
if(psi & DACL_SECURITY_INFORMATION)
{
  if(pSDControl & SE_DACL_PROTECTED)
  {/** The DACL must not inherit. Switch psi to
   *   PROTECTED_DACL_INFORMATION
   **/
    psi |= PROTECTED_DACL_SECURITY_INFORMATION;
    dwErr = SetSecurityDescriptorControl(ppSD, SE_DACL_PROTECTED,
      SE_DACL_PROTECTED);
  }
  if(pSDControl & SE_DACL_AUTO_INHERIT_REQ)
  {/* We must toggle the DACL's inheritance. */
    psi |= UNPROTECTED_DACL_SECURITY_INFORMATION;
    dwErr = SetSecurityDescriptorControl(ppSD,
      SE_DACL_AUTO_INHERIT_REQ, SE_DACL_AUTO_INHERIT_REQ);
  }
}
if(psi & SACL_SECURITY_INFORMATION)
{
  if(pSDControl & SE_SACL_PROTECTED)
  {/* ...And SACL */
    psi |= PROTECTED_SACL_SECURITY_INFORMATION;
    dwErr = SetSecurityDescriptorControl(ppSD, SE_SACL_PROTECTED,
      SE_SACL_PROTECTED);
  }
  if(pSDControl & SE_SACL_AUTO_INHERIT_REQ)
  {/* ...And SACL. */
    psi |= UNPROTECTED_SACL_SECURITY_INFORMATION;
    dwErr = SetSecurityDescriptorControl(ppSD,
      SE_SACL_AUTO_INHERIT_REQ, SE_SACL_AUTO_INHERIT_REQ);
  }
}
...
/* To Figure 54. */

Figure 53: Remapping the Control bits to SECURITY_INFORMATION flags.

If your object is a container, then it is likely you need to add support for the sledgehammer checkbox.

8. Handling the Sledgehammer Checkboxes.

It's not documented in the SDK, but in order to detect if the user selected one of the sledgehammer checkboxes, you need to test the psi parameter for the presence of one of the following flags: SI_OWNER_RECURSE or SI_RESET_DACL_TREE or SI_RESET_SACL_TREE. If any of these flags are present, you will need to recursively apply the security descriptor to all child objects. You can guarantee that this code will involve collections somehow.

Since ACLUI knows nothing about your objects, it doesn't know how to walk them, so you need to walk the directory tree yourself. Walking a directory tree shouldn't be terribly difficult. It's simply FindFirstFile(), FindNextFile(), and FindClose() (or the equivalent function if your object is not a folder). Then with the list of files obtained, set each security descriptor one-by-one.

This technique may seem to work, but it suffers from a flaw. Suppose you are currently denied access to the folder. In that case, FindFirstFile() will fail, and you won't be able to walk the tree. How do we walk a directory for which we currently have no access? Don't dismiss this problem, resetting the security is a major use case for your application (the user has just been locked out of a folder, and now needs you to bail them out).

The answer here is to cheat! You can't walk the file system, because the access checking mechanism is preventing us from doing so. What we need to do is disable the access checking mechanism altogether! If you have the "SeBackupPrivilege", you can disable the access checking mechanism, open any file with FILE_TRAVERSE rights and successfully walk all child objects! This is what Windows does for Windows 2000. Unfortunately, this "off" switch is only available if you are a member of the BackupOperators / Administrators group. How can we walk the folder if we aren't an administrator?

If you're on Windows XP or later, you can use the recently documented TreeResetNamedSecurityInfo() API. TreeResetNamedSecurityInfo() behaves like the normal SetSecurityInfo(), but can also walk a file-system folder or a registry key and set the security for all children. It works by impersonating a highly privileged account, and using the privileged account to walk the tree. Optionally, you can supply a callback function that you can use to receive notifications about each file. If you need to cancel the tree walking operation, you should set the PROG_INVOKE_SETTING member to ProgressCancelOperation. Filepermsbox makes use of the callback to provide a progress dialog.

This method bears the advantage that it is a supported API, thus will be more compatible with future Windows versions. However, TreeResetNamedSecurityInfo() is a recent API not available in Windows 2000, and has its own share of undiscovered bugs.

VOID WINAPI TreeCallBackFunc(wchar_t *pObjectName, DWORD Status,
  PROG_INVOKE_SETTING *pInvokeSetting, void *PtrToThis, BOOL SecuritySet)
{

  CSecurityInformation *ThisClass =
    reinterpret_cast<CSecurityInformation *>(PtrToThis);

  if(SecuritySet != TRUE)
  {/* Something occurred. Inform the User. */
    INT_PTR Choice = DisplayErrorDialog(pObjectName, ThisClass, Status);
    /* Choice will be one of: IDCANCEL, IDRETRY, IDIGNORE, IDC_BUTTON1 */

    switch(Choice)
    {
      default:
      case IDCANCEL:
        *pInvokeSetting = ProgressCancelOperation;
        break;
      case IDRETRY:
        *pInvokeSetting = ProgressRetryOperation;
        return;
      case IDIGNORE:
        *pInvokeSetting = ProgressInvokeEveryObject;
        break;
      case IDC_BUTTON1:
        *pInvokeSetting = ProgressInvokeNever;
        break;
     }
  }
  else *pInvokeSetting = ProgressInvokeEveryObject;
}


{
  /* Continued from Figure 53 */
  ...
  if((psi & SI_OWNER_RECURSE || psi & SI_RESET_DACL_TREE ||
    psi & SI_RESET_SACL_TREE) && this->IsContainer(ObjectName))
  {/* Call the XP provided function. */
    psi = psi &~ ( SI_OWNER_RECURSE | SI_RESET_DACL_TREE |
      SI_RESET_SACL_TREE);
    dwErr = this->pfnTreeResetNamedSecurityInfo(ObjectName,
      this->SeObjectType, psi, SidOwner, SidGroup, NewDAcl,
      NewSAcl, this->KeepExplicit, TreeCallBackFunc,
      ProgressInvokeEveryObject, reinterpret_cast<void *>(this));
    SetLastError(dwErr);
  }
  else
  {
    psi = psi &~ ( SI_OWNER_RECURSE | SI_RESET_DACL_TREE | SI_RESET_SACL_TREE);
    SetLastError(ERROR_SUCCESS);
    dwErr = SetNamedSecurityInfo(ObjectName, this->SeObjectType, psi, SidOwner,
      SidGroup, NewDAcl, NewSAcl);
    SetLastError(dwErr);
  }

  ...
  /* Cleanup, update shell and inform user */
}

Figure 54: Setting security with the help of TreeResetNamedSecurityInfo().

If you intend to support Windows 2000 clients, you'll need to emulate the TreeResetNamedSecurityInfo() API. That leads us to the final option: walk the directory manually, with a tweaked traversal algorithm.

This is one of those few cases where depth-first recursion is a better choice than breadth-first recursion. When you're currently denied access to the object, you have to write the security descriptor first, then read the directory contents. This works if you are granting yourself access, but what if you're denying access to yourself? That is, you currently have access to the directory, but you are about to write a security descriptor that will deny access to yourself. Once you write to the main folder, you can no longer read the directory. In this scenario, you need to traverse the directory first, then set the security descriptor. That's two different algorithms you will need.

Filepermsbox solves this dilemma by applying both algorithms. Get the list of files before, recurse into all these children, then apply the security descriptor to the root folder, then get the list of files after, and repeat the recursion.

If you are faced with this problem, I recommend you find a way of bypassing or switching off the access check mechanism (e.g. enable a privilege, or contact a system service). Trying to engineer an algorithm that handles access checks is quite cumbersome.

If you are writing the DACL or SACL, there is one more task. Although the root folder should get the security descriptor manufactured by the user, the folder's children should get a different security descriptor: "D:ARAIS:ARAI". That is, all children should get an empty security descriptor that auto-inherits. This security descriptor will erase all explicit ACEs, leaving only the auto-inherited ACL from the root folder. For Filepermsbox, this happens to be the same as the default security descriptor (from ISecurityInformation::GetSecurity()). If you were using TreeResetNamedSecurityInfo(), you could make use of the KeepExplicit parameter to perform this for you.

DWORD CSecurityInformation::TreeResetWin2k(TCHAR *m_ObjectName,
  PSECURITY_DESCRIPTOR ppSD, SECURITY_INFORMATION psi)
{

  /* The default security descriptor needs
     to be applied to all children of this object. */
  PSECURITY_DESCRIPTOR pSD2 = NULL;
  if(psi & DACL_SECURITY_INFORMATION || psi & SACL_SECURITY_INFORMATION)
    dwErr = this->GetSecurity(psi, &pSD2, TRUE);

  if(pSD2 == NULL)
  {/* If default security cannot be used,
      use a copy of the current security descriptor. */
    LPTSTR lpszSD = NULL;
    ConvertSecurityDescriptorToStringSecurityDescriptor(ppSD, 
        SDDL_REVISION_1, psi, &lpszSD, NULL);

    ConvertStringSecurityDescriptorToSecurityDescriptor(lpszSD, 
       SDDL_REVISION_1, &pSD2, NULL);
    LocalFree(lpszSD); lpszSD = NULL;
  }

  /* Apply ppSD to the root object, and pSD2 to all children. */

  ...
  LocalFree(pSD2); pSD2 = NULL;
}

Figure 55: Creating a security descriptor to apply to children.

9. Handling more than one object.

Someone asked this once question in the newsgroups, so I added this section. Can the ACL editor work for more than one object? At first glance, of course it can't. The ACL editor is only capable of showing one security descriptor, and that means you can only show one object. How in the world can the ACL editor edit more than one object?

It can show it if the objects have equal security descriptors. If the two objects have equal security descriptors, only one will need displaying, and you can apply it to both. However, what constitutes an equal security descriptor? There is no API called IsEqualSecurityDescriptor() to help us, so we need to compare the security descriptors ourselves. This is not only useful for the ACL editor, but can be used in security auditing applications (e.g. AccessEnum [^]).

Two security descriptors can be considered equal if AccessCheck() behaves in the same way for both of them. The group, owner, version, and persistent control bits (SE_DACL_AUTO_INHERIT_REQ is a temporary flag and doesn't count) need to be equal for both security descriptors. For the DACL and SACL, not only do they have to have the same entries, you have to take into account the proper ordering of the ACEs.

  1. If either the group, owner, persistent control flags, or version are different, then there is no equality.
  2. If the DACLs have differing entries (i.e. one grants access to yourself, whereas the other one denies access), then they can't be equal (AccessCheck() will behave differently).
  3. If an Allow ACE precedes a Deny ACE, then the disordering of the ACLs will change the behaviour of AccessCheck().
  4. However, if two allow ACEs are in the wrong order, the proper ordering of ACEs will still be upheld, and the AccessCheck() would be the same for both. In general, any change which preserves the ordering of the DACL will not change the security descriptor.
  5. 2, 3 and 4 should apply to the SACL as well as the DACL.

It's quite clear you will need to split up the security descriptor back into its parts to perform the equality. Splitting up a security descriptor was explained in part 2. For the DACL / SACL, you could perform a byte-wise comparison (which is what Windows does), but that means you will mistake case 4 as an unequal security descriptor. Filepermsbox goes a little further--it takes one ACE in the first DACL and searches for this value in the second DACL. This method successfully passes case 4, but if the DACL is disordered (case 3), then Filepermsbox will mistakenly pass the DACL. The final test you need to perform is checking the proper ordering of ACEs.

So in summary, two security descriptors are equal if:

  1. The group and owner SIDs are the same.
  2. The version and persistent control flags are the same.
  3. The DACLs and SACLs contain the same number of entries.
  4. The DACLs and SACLs contain the same entries.
  5. The DACLs and SACLs are in canonical order.

If you can utilise .NET / ATL / SDDL / Regular Expressions, by all means make use of them to help you.

If you have found that the security descriptors are not equal, you can ask the user if they want to change the security descriptor so they are equal (this is what Windows does). Once you have made the security descriptors equal, you can just display the first security descriptor. The user will edit just the first security descriptor, and when the user presses Apply, your SetSecurity() method is called. All you need to do now is apply this one security descriptor to all the selected objects in a loop (shouldn't be too difficult if you implemented the sledgehammer functionality).

With everything done, it's now a matter of calling SetPrivateObjectSecurityEx(), SetSecurityInfo(), SetNamedSecurityInfo() or TreeResetNamedSecurityInfo() as appropriate. Then clean up the privileges and resources, and return any errors back to ACLUI.

STDMETHODIMP CSecurityInformation::SetSecurity
  (SECURITY_INFORMATION psi, PSECURITY_DESCRIPTOR pSD)
{
  ...
  /* Initialise UI, enable privileges, etc. */

  std::list< const std::basic_string<TCHAR> > DirectoryCollection;
  /* ... Fill in DirectoryCollection... */


  for(std::list <const std::basic_string<TCHAR> >::const_iterator
    m_ObjectName = DirectoryCollection.begin();
    m_ObjectName != DirectoryCollection.end(); m_ObjectName++)
  {/* Use our helper class to convert the wstring
      into a writable TCHAR array */
    sized_array<TCHAR> FinalName(*m_ObjectName);

    dwErr = this->SetSecurityForObject(FinalName.get(), psi, pSD);
    /* Call Figure {49 50 51} for this filename */

    if(dwErr != ERROR_SUCCESS) break;
  }

  /* Cleanup, handle any errors, etc. */
  ...

  return HRESULT_FROM_WIN32(dwErr);
}

Figure 56: Writing security descriptors for a collection of objects.

32. ISecurityInformation2, ISecurityObjectTypeInfo, and IEffectivePermission

After all that work implementing ISecurityInformation (particularly SetSecurity()), you'd probably want to rest on your laurels and move onto the next project now. However, there are three other interfaces that your class can implement. If you choose not to implement the other three interfaces, you'll lose out on some quite useful features. If you are implementing these interfaces, don't forget to inform ACLUI by updating IUnknown::QueryInterface() and ISecurityInformation::GetObjectInformation().

10. ISecurityObjectTypeInfo::GetInheritSource() and INHERITED_FROM[ ]

You only need this method if your class implements ISecurityObjectTypeInfo. This method is only available if the operating system is Windows XP and above. This method enables the ACL editor to show where an inherited ACE came from. It will show the result in the Advanced DACL page. In order to implement this interface, you need to make use of the GetInheritanceSource(), which fills out (drum rolls)... a hash table!

Once GetInheritanceSource() is called, it is good practice to call FreeInheritedFromArray() on the returned INHERITED_FROM array to free up the memory.

The ACL editor will not do this for you. You must implement code that copies this array to a LocalAlloc()'ed one, and return the LocalAlloc()'ed array instead. The operating system does free the block with a LocalFree() (even though it's not documented). One final problem will be how to free the contents of those strings you copied. Filepermsbox reuses the technique used in section 13 and mentioned in the C FAQ [^], which is to allocate a big block of memory and use some creative pointer arithmetic to mimic a two dimensional array.

This is a rather questionable design decision made by Microsoft to implement this API, and we have to resort to this unclean hack.

STDMETHODIMP CSecurityInformation::GetInheritSource
  (SECURITY_INFORMATION psi, PACL pAcl, PINHERITED_FROM *ppInheritArray)
{
  DWORD dwErr = 0;
  size_t i = 0, dwSize = 0;
  PINHERITED_FROM InheritTmp = reinterpret_cast<PINHERITED_FROM>
    (LocalAlloc(LPTR, (1 + pAcl->AceCount) * sizeof(INHERITED_FROM)));

  BOOL Container = (this->m_dwSIFlags & SI_CONTAINER) ? TRUE : FALSE;

  dwErr = GetInheritanceSource(this->FileName, SE_FILE_OBJECT, psi,
    Container, NULL, 0, pAcl, NULL, &g_ObjMap, InheritTmp);

  /* Find the required size of our 2 dimensional array */
  for(i = 0 ; i < pAcl->AceCount ; i++)
  {
    const std::basic_string<TCHAR> &GenName =
      InheritTmp[i].AncestorName;
    dwSize += GenName.size() + 1;
  }

  /* Allocate the data all in one. */
  PINHERITED_FROM InheritResult =
    reinterpret_cast<PINHERITED_FROM>(LocalAlloc(LPTR, (1 + pAcl->AceCount) *
      sizeof(INHERITED_FROM) + dwSize * sizeof(TCHAR)));

  /** It may help if we can get a pointer to the data segment as well as the
  *   array header.
  **/
  TCHAR *DataPtr = reinterpret_cast<TCHAR *>(reinterpret_cast<BYTE *>
    (InheritResult) + pAcl->AceCount * sizeof(INHERITED_FROM));

  for(i = 0 ; i < pAcl->AceCount ; i++)
  {
    const std::basic_string<TCHAR>
      &GenName = InheritTmp[i].AncestorName;
    GenName.copy(DataPtr, GenName.size(), 0);
    /* copy over the strings */

    /* Make the headers point to the newly created data */
    InheritResult[i].GenerationGap = InheritTmp[i].GenerationGap;
    InheritResult[i].AncestorName = DataPtr;

    /* and move the pointer forward */
    DataPtr += GenName.size() + 1;
  }

  /* Cleanup */
  FreeInheritedFromArray(InheritTmp, pAcl->AceCount, NULL);
  LocalFree(InheritTmp); InheritTmp = NULL;

  *ppInheritArray = InheritResult;
  return HRESULT_FROM_WIN32(dwErr);
}

Figure 57: Finding the origin of an auto-inherited ACE.

11. IEffectivePermission::GetEffectivePermission()

You only need this method if your class implements IEffectivePermission. This method is only available if the operating system is Windows XP and above. The ACL editor uses this function to implement its Effective Permissions feature (this provides a user interface for the GetEffectiveRightsFromAcl() API). To show the "Effective Permissions" tab, you'll need to specify an undocumented flag in your SI_OBJECT_INFO struct (SI_EDIT_EFFECTIVE). To implement it in your code you have four tasks to perform:

  1. Extract the ACL from the provided security descriptor.
  2. Allocate an array of ACCESS_MASKs.

  3. Build a TRUSTEE from the supplied SID. If you need to translate the SID to a name, make sure you do the SID lookup using the supplied pszServerName parameter.
  4. Call GetEffectiveRightsFromAcl() with the buffer you allocated in step 2, and the TRUSTEE you built with step 3. Then set the output parameters to the output results.

The reason you should LocalAlloc() the array of ACCESS_MASKs is because the ACL editor will call LocalFree() on the buffer once it has finished with it. If your object supports GUIDs, you'll have a bit of extra work to do. You must fill out a OBJECT_TYPE_LIST[] array with the property tree for the object (along with the GUIDs).

STDMETHODIMP CSecurityInformtion::GetEffectivePermission
  (const GUID *, PSID pUserSid, LPCWSTR /*pszServerName*/,
  PSECURITY_DESCRIPTOR pSD, POBJECT_TYPE_LIST* ppObjectTypeList,
  ULONG* pcObjectTypeListLength, PACCESS_MASK* ppGrantedAccessList,
  ULONG* pcGrantedAccessListLength)
{
  DWORD dwErr = 0 ;
  BOOL AclPresent = FALSE, AclDefaulted = FALSE ;
  PACCESS_MASK AccessRights = NULL ;
  PACL Dacl = NULL ;

  *ppObjectTypeList = const_cast<OBJECT_TYPE_LIST *>(g_DefaultOTL) ;
  *pcObjectTypeListLength = 1 ;

  GetSecurityDescriptorDacl(pSD, &AclPresent, &Dacl, &AclDefaulted) ;
  TRUSTEE Trustee = {0} ;
  BuildTrusteeWithSid(&Trustee, pUserSid) ;

  AccessRights = reinterpret_cast<PACCESS_MASK>
    (LocalAlloc(LPTR, sizeof(PACCESS_MASK) + sizeof(ACCESS_MASK))) ;

  dwErr = GetEffectiveRightsFromAcl(Dacl, &Trustee, AccessRights) ;

  *ppGrantedAccessList = AccessRights ;
  *pcGrantedAccessListLength = 1 ;
  return S_OK ;
}

Figure 58: Implementing the undocumented Effective Permissions tab.

12. ISecurityInformation2::LookupSids() and SID_INFO_LIST

Sometimes, the ACLUI will be unable to translate SIDs from binary form into a user name. It just can't quite pass in the right parameters to LookupAccountSid(), or maybe LsaLookupSids() is required. In these cases, you may be able to translate the SIDs. If you override ISecurityInformation2::LookupSids(), you can override the SID lookup process, thus allowing you to translate the untranslatable SID.

LookupSids() passes you an array of SIDs. You are expected to translate as many as you can, and put the resultant principal / display names in an array of SID_INFO structures (a SID_INFO_LIST). To return the data back to ACLUI, you are expected to wrap the structure in an IDataObject. That means implementing another COM interface (only IDataObject::GetData() needs to be implemented). Since the SID_INFO_LIST must last at least as long your CDataObject, you should make your CDataObject manage the SID_INFO_LIST. Filepermsbox does not implement this interface (J. Brown [^] should help you better than I). However, if you have created a CDataObject, your LookupSids implementation can look like Fig. 53.

STDMETHODIMP CSecurityInformation::LookupSids(ULONG /* cSids */,
  PSID * /* rgpSids */, LPDATAOBJECT * /* ppdo */)
{/* ISecurityInformation2. Let ACLUI handle this work. */
  SID_INFO_LIST *sidList = new SID_INFO_LIST[(sizeof(ULONG) +
    cSids * sizeof(SID_INFO)) / sizeof(SID_INFO_LIST) + 1];
  memset(sidList, 0, sizeof(ULONG) + cSids * sizeof(SID_INFO));
  for(ULONG i = 0; i < cSids; i++)
  {
    SID_NAME_USE peUse = SidTypeUnknown;
    DWORD cchName = 0, cchReferencedDomainName = 0;
    LookupAccountSid(NULL, rgpSids[i], NULL,
      &cchName, NULL, &cchReferencedDomainName, &peUse);
    TCHAR *UserName = new TCHAR[cchName];
    memset(UserName, 0, sizeof(TCHAR) * (cchName));
    TCHAR *DomainName = new TCHAR[cchReferencedDomainName];
    memset(DomainName, 0, sizeof(TCHAR) * (cchReferencedDomainName));

    LookupAccountSid(NULL, rgpSids[i], UserName, &cchName,
      DomainName, &cchReferencedDomainName, &peUse);
    sidList->aSidInfo[i].pSid = rgpSids[i];
    sidList->aSidInfo[i].pwzClass = _T("User");
    sidList->aSidInfo[i].pwzCommonName = UserName;

    std::basic_string<TCHAR> UPNName = UserName;
    UPNName.append(_T("@"));
    UPNName.append(DomainName);

    TCHAR *lpszUPN = new TCHAR [UPNName.size() + 1];
    UPNName.copy(lpszUPN, UPNName.size(), 0);
    lpszUPN[UPNName.size()] = _T('\0');
    sidList->aSidInfo[i].pwzUPN = lpszUPN;
    sidList->cItems++;
    /* Domainname is not referenced in the final array, so delete it here. */
    delete [] DomainName; DomainName = NULL;
  }

  CDataObject *outObj = new CDataObject(sidList);
  *ppdo = dynamic_cast<IDataObject *>(outObj);
  /** In the destructor for outObj, you are responsible for deleting sidList
  *   and its contents.
  **/
  return S_OK;
}

Figure 59: Translating a SID to user name.

13. ISecurityInformation2::IsDaclCanonical()

Earlier on, I told you that the ACL editor sorts the ACLs so they obey the proper ordering rules. If you attempt to hand it a disordered ACL, ACLUI will sort those entries into the correct order. If you implement this method, you can override the ACL sort. The method is rather simple: you are handed a pointer to ACL, and you should return TRUE if the ACL order is correct, or FALSE otherwise. As for checking if an ACL is sorted, that's left as an exercise for the reader.

Filepermsbox needs the ordering of the ACLs to always be correct, so it always returns FALSE (it doesn't implement this method).

33. Presenting the interface

If you have reached this far into the series, you have successfully implemented an ISecurityInformation object, ready to be called. All that's left now is to open up the ACL editor. You can call either CreateSecurityPage() (if you have a property sheet dialog), or EditSecurity() (if you don't). Any of these functions will show the ACL editor.

DoSecurityBox
  (HWND TheirhWnd,
   SE_OBJECT_TYPE seObjType,
   std::list<const std::basic_string<TCHAR> > &FileNames)
{
  CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
  {/* SecPage needs to be destructed before CoUninitialize. */
    std::auto_ptr<CSecurityInformation> SecPage
      (new CSecurityInformation(FileNames, seObjType, TheirhWnd));
    EditSecurity(TheirhWnd, SecPage.get());
  }
  CoUninitialize();
  return TRUE;
}

Figure 60: Showing the ACL editor.

And that's it! Filepermsbox adds on the following user interface elements...

14. Adding a Progress Dialog.

ISecurityInformation::SetSecurity() could take a long time to complete. (It sure took a long time to code!) Therefore, you may want to consider adding a progress dialog. Since there is a high probability for failure (you usually aren't allowed to write security descriptors), you should inform the user whenever there is a failure, and this is where a progress dialog comes into its own. I've chosen Michael Dunn's progress dialog (here). It's placed on a separate worker thread, created before the ACL editor is created (I couldn't create a thread in ISecurityInformation::SetSecurity(), because the thread is impersonating at that moment). To support the Cancel button, I've made use of the callbacks in TreeResetNamedSecurityInfo().

15. The Property Sheet Extension

Filepermsbox optionally provides a shell extension that's associated for all files and folders. The shell extension is based on the SDK samples (with some help provided by the "Complete Idiot's Guide to Writing Shell Extensions"). This is one of the few shell extensions that work for multiple files and multiple folders. When you select multiple files and folders, Filepermsbox checks each file for their security descriptor. If security descriptors aren't available (e.g. FAT32 drive), then the property sheet is not shown.

All security descriptors must be equal, otherwise the user is prompted to reset the security descriptors. Clearly, if the files come from different paths (e.g. one file comes from "C:\Windows" and the other comes from "C:\Program Files\Common Files"), then of course the security descriptors are going to be different. But in this case, you know why they're different. Any attempt at resetting the security descriptor will not make them equal—it will only make things worse. Windows suffers from exactly this bug. Filepermsbox does not. (Filepermsbox parses the paths to see which directory they come from. It does not show the tab in this case.)

Before showing the ACL editor, the property sheet shows you the SDDL form of the security descriptor.

Filepermsbox`s SDDL Viewer

Figure 61: A view of the Filepermsbox property sheet dialog.

16. Filepermsbox API.

Right now, the Filepermsbox API is totally brain dead. There's only one API available, and it's only usable if you have Visual C++ 2003 (this is due to the FileNames parameter):

/** Displays the ACL editor for the filenames specified, so
*   the user can edit their security descriptor.
*
*   All parameters are in parameters
*
*   HWND TheirhWnd - The parent window
*   SE_OBJECT_TYPE seObjType - The type of object specified.
*     Currently, only SE_FILE_OBJECT is supported.
*   const list<const wstring> &FileNames - The list of
*     filenames the ACL editor will display. Only the first
*     security descriptor will be passed to the ACL editor.
*   BOOL AddWritability - Unless set to TRUE, the ACL editor
*     will be read-only, meaning the user can't make any
*     changes.
*
*   Returns TRUE for success and FALSE for failure. A error
*   message may be displayed to the user if a unhandled
*   exception occurs. You might get more information from
*   GetLastError().
**/
USRDLL_IMPORT BOOL __stdcall DoSecurityBox(HWND TheirhWnd,
   SE_OBJECT_TYPE seObjType,
   const std::list<const std::basic_string<TCHAR> > &FileNames,
   BOOL AddWritability);

Figure 62: The main API exported from Filepermsbox.dll.

I've created a more usable .NET wrapper in C++ / CLI (you can call it via OLE automation), which instead of asking for an STL list, asks for a System.String[]. However, since you need Visual Studio 2005 to use it, it's not currently available. When Visual Studio 2005 goes final, I will recompile the solution in VS2005, then you can use my API.

34. Final Thoughts.

ACL-based security was envisioned in the very first months of Windows NT (back in 1989). It remains the most complicated of the different types of authorization. ACL-based security is centered around one function: AccessCheck(). All the structures, ACLs, privileges, and security descriptors exist just to customize the behaviour of AccessCheck(). The primary structure that customises this API is the SECURITY_DESCRIPTOR struct.

The original authorization API was universally deemed unusable by developers, including those in Microsoft. It was far too difficult to create security descriptors that maintain the correct balance between functionality and security. Hence, developers usually shunned security descriptors completely or made substandard security descriptors. However, these poorly chosen security descriptors have led to some of the worst security holes (and the most damaging viruses to go with it) the IT industry has ever seen. Microsoft (and others) realised the difficulty of using ACL-based security, and created new interfaces for the ACL-based functions (examples are in C++, ATL, and most recently .NET).

Despite their complexity, security descriptors can be applied to any object you like with the help of authorization APIs, but are most commonly applied to files, kernel handles and registry keys. The authorization API relies on the authentication API doing its job properly, and borrows a number of structures from the authentication API, such as the SID, access token and impersonation level.

In this series, you were shown how to program ACLs using the older method, and using three of the available security libraries. With that in place, you were shown how to implement the ACL editor in your applications so you can allow the user to edit security descriptors.

I hope you have enjoyed my series on the Windows Access Control Model. The series was designed to cover just enough topics for you to program the ACL editor, which is why it is quite thin on security topics. We didn't talk about role-based security (a simpler type of security model which secures according to which group you belong), or code based security (another simpler model which secures by what action you are taking). These newer security models are best exploited with the help of the .NET framework (though they are available for native applications).

Active directory security, based on private security descriptors, was conveniently glossed over in this series. However, since private security descriptors can get extremely complex, especially for administrators, Microsoft has provided an alternative scripting API for them. The Resource Kit / Technical Reference are good references for studying Active Directory Security.

Even if you haven't benefited from this series, I certainly have benefited from writing it. Thanks to this series, I was able to update Filepermsbox to v1.10, with these fixes:

  • The privilege code has been rewritten. This fixes the leaked privilege bug. The privilege is now only enabled for very short periods, and is no longer process wide.
  • The progress dialog code has been rewritten to be more responsive, more accurate, and more stable.
  • The "sledgehammer checkbox" algorithm has been rewritten. Win2K should be much more stable.
  • Updated the code to compile with VS2005.
  • Started a developer API (you'll have to wait for .NET v2.0 to be released however).
  • The code no longer crashes if you have invalid filenames.
  • Remote files now have their SIDs translated properly.
  • Win9x no longer crashes when you run this app.
  • Started to generalize the app (so it doesn't only work for files).

Bibliography / Further Reading

I obtained most of the material for this series from the books:

There are also many good articles out there on the internet; here're some starters:

If you want test programs:

35. History

  • 10/9/2004
    • Development started on QueryServiceConfig, a service security editor.
  • 12/12/2004
    • QueryServiceConfig was altered to handle files instead of services. Thus, Filepermsbox was born.
  • 24/12/2004
    • Filepermsbox and QueryServiceConfig were released.
  • 12/2004 - 3/2005
    • Filepermsbox was updated through to v1.09. QueryServiceConfig to v1.03.
  • 24/3/2005
    • Started work documenting my experiences for CodeProject.
  • 26/3/2005
    • Split article into four parts.
  • 6/4/2005
    • Part 1 of the series was uploaded.
  • 12/4/2005
    • Moved a couple of sections from part 2 back into part 1. Fixed the horizontal scroll bar whilst I was at it.
  • 23/4/2005
    • Part 2 of the series was uploaded. This contains an updated TOC.
  • 14/5/2005
    • Part 3 of the series was uploaded. TOC updated again.
  • 21/6/2005
    • The entire series is now up. All articles were updated to reflect the change.
  • 07/09/2005
    • Updated Filepermsbox to v1.11.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United States United States
Mr. Shah is a reclusive C++/C# developer lurking somewhere in the depths of the city of London. He learnt physics at Kings' College London and obtained a Master in Science there. Having earned an MCAD, he teeters on the brink of transitioning from C++ to C#, unsure of which language to jump to. Fortunately, he also knows how to use .NET interop to merge code between the two languages (which means he won't have to make the choice anytime soon).

His interests (apart from programming) are walking, football (the real one!), philosophy, history, retro-gaming, strategy gaming, and any good game in general.

He maintains a website / blog / FAQ / junk at shexec32.serveftp.net, where he places the best answers he's written to the questions you've asked. If you can find him, maybe you can hire Mr. Shah to help you with anything C++[/CLI]/C#/.NET related Smile | :) .

Comments and Discussions

 
GeneralSolution to Translate SIDs to NTAccount in Crossdomain situations, when you get the funny IdentityNotMappedException Pin
pamkkkkk11-Nov-09 21:52
pamkkkkk11-Nov-09 21:52 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.