This post includes code for setting permissions on public folders. It works by obtaining the xml describing the existing permissions and then using that as the basis of the xml sent with a PROPPATCH WebDAV command to udate the permissions. I found it very difficulty to find documentation that described how to put together the xml for all of the problems in this series of posts, but especially when I wanted to set permissions on folders - there are rules about the order that ACE's appear in the cml document etc. Fortunately I came across the PFDAVAdmin tool which helped considerably. This tool allows you to perform administrtive tasks such as setting the permissions on public folders, but before you commit any changes you can view the xml that will be sent to the server - I used this as the starting point for coming up with the code below:
/// <summary>
/// Gives the object identified by the SID the "author" role on the folder identified by the URI.
/// </summary>
/// <param name="cookies">The cookies returned from previously authenticating.</param>
/// <param name="URI">The folder to allow access to.</param>
/// <param name="sid">The SID of the object to give access to the folder.</param>
public static void AddACEToExchangePublicFolder(CookieCollection cookies,
string URI,
SecurityIdentifier sid)
{
UpdateExchangePublicFolderPerms(cookies, URI, delegate(XmlNamespaceManager xmlnsManager, XmlDocument xmlPropUpdate)
{
// These access masks together specify the exchange "author" role - these were discovered using
// PFDAVAdmin.
InsertGroupAceXML(xmlnsManager, xmlPropUpdate, "//S:effective_aces", sid, "1208ab", "dc914");
InsertGroupAceXML(xmlnsManager, xmlPropUpdate, "//S:subcontainer_inheritable_aces", sid, "1208ab", "dc914");
InsertGroupAceXML(xmlnsManager, xmlPropUpdate, "//S:subitem_inheritable_aces", sid, "120ea9", "1f0716");
});
}
/// <summary>
/// Delegate used to update the security descriptor in the xmlPropUpdate xml.
/// </summary>
/// <param name="xmlnsManager">Namespace manager.</param>
/// <param name="xmlPropUpdate">A property update WebDAV query. This is expected to contain acls
/// that describe the original state of a public folders permissions. This can the be edited to
/// add or remove entries</param>
private delegate void UpdateSecDescrXml(XmlNamespaceManager xmlnsManager, XmlDocument xmlPropUpdate);
/// <summary>
/// Updates an exchange public folders permissions. A propertyupdate WebDAV query is created to begin
/// with, which would not make any changes to the folders permissions. This is then passed to the
/// UpdateSecDescrXml delegate to add or remove permissions as required.
/// </summary>
/// <param name="cookies">The cookies returned from previously authenticating.</param>
/// <param name="URI">The folder to allow access to.</param>
/// <param name="update">The delegate to use to add or remove permissions.</param>
private static void UpdateExchangePublicFolderPerms(CookieCollection cookies,
string URI,
UpdateSecDescrXml update)
{
UpdateExchangePublicFolderPerms(cookies, URI, URI, update);
}
/// <summary>
/// Updates an exchange public folders permissions. A propertyupdate WebDAV query is created to begin
/// with, based on the from folders permissions. This is then passed to the UpdateSecDescr delegate to
/// add or remove permissions as required. Finally the update is applied to the to folder.
/// </summary>
/// <param name="cookies">The cookies returned from previously authenticating.</param>
/// <param name="fromURI">The from flder.</param>
/// <param name="toURI">The to folder.</param>
/// <param name="update">The delegate to use to add or remove permissions.</param>
private static void UpdateExchangePublicFolderPerms(CookieCollection cookies,
string fromURI,
string toURI,
UpdateSecDescrXml update)
{
// This WebDAV query will return the permissions currently applied to the folder - in other words
// the ones inherited from it parent or in reality, the administrative group it is a part of.
StringBuilder propfind = new StringBuilder();
propfind.Append("<?xml version=\"1.0\"?>");
propfind.Append("<d:propfind xmlns:d=\"DAV:\">");
propfind.Append("<d:prop xmlns:S=\"http://schemas.microsoft.com/exchange/security/\">");
propfind.Append("<S:descriptor/>");
propfind.Append("</d:prop>");
propfind.Append("</d:propfind>");
using (HttpWebResponse response = SendPropFindRequest(cookies, fromURI, "0", propfind.ToString()))
{
using (Stream stream = response.GetResponseStream())
{
XmlDocument xmlOrigSec = new XmlDocument();
xmlOrigSec.Load(stream);
// Need to get the security descriptor from the xml returned, then add the new aces to that
// and perform a propupdate to apply the new set of aces.
StringBuilder propUpdate = new StringBuilder();
propUpdate.Append("<?xml version=\"1.0\"?>");
propUpdate.Append("<d:propertyupdate xmlns:d=\"DAV:\" xmlns:S=\"http://schemas.microsoft.com/security/\" xmlns:exsec=\"http://schemas.microsoft.com/exchange/security/\">");
propUpdate.Append("<d:set><d:prop><exsec:descriptor>");
propUpdate.Append("</exsec:descriptor></d:prop></d:set></d:propertyupdate>");
XmlDocument xmlPropUpdate = new XmlDocument();
xmlPropUpdate.LoadXml(propUpdate.ToString());
// Get the current security descriptor and add it to the propupdate xml
XmlNamespaceManager xmlnsManager = new XmlNamespaceManager(xmlPropUpdate.NameTable);
xmlnsManager.AddNamespace("exsec", "http://schemas.microsoft.com/exchange/security/");
xmlnsManager.AddNamespace("S", "http://schemas.microsoft.com/security/");
XmlNode secDescrNode = xmlPropUpdate.DocumentElement.SelectSingleNode("//exsec:descriptor", xmlnsManager);
XmlNode origSecDescrNode = xmlOrigSec.DocumentElement.SelectSingleNode("//S:security_descriptor", xmlnsManager);
XmlNode origDaclNode = xmlOrigSec.DocumentElement.SelectSingleNode("//S:dacl", xmlnsManager);
XmlNode newSecDescrNode = secDescrNode.AppendChild(xmlPropUpdate.ImportNode(origSecDescrNode, false));
newSecDescrNode.AppendChild(xmlPropUpdate.ImportNode(origDaclNode, true));
// Update the security descriptor xml
if (update != null)
{
update(xmlnsManager, xmlPropUpdate);
}
TidyPropUpdate(xmlnsManager, xmlPropUpdate);
HttpWebResponse response2 = SendRequest(cookies, toURI, "PROPPATCH", null, xmlPropUpdate.InnerXml);
response2.Close();
}
}
}
/// <summary>
/// Inserts the access allowed and access denied ace's for the group sid in the acesXPath section of doc.
/// The order that aces appear is important - they must appear in Exchange canonical order which dictates
/// that ace's for users must appear first, then groups allow entries, then group deny entries and then
/// finally the access denied ace for the everyone or default group. This means that this method as it
/// stands can *only* be used to add aces for groups.
/// </summary>
/// <param name="xmlnsManager">Namespace mappings required when performing XPath queries.</param>
/// <param name="doc">The document the aces are to be inserted into.</param>
/// <param name="acesXPath">The xpath to the section the aces are to be inserted e.g. //S:effective_aces.</param>
/// <param name="sid">The sid of the group to set permissions for.</param>
/// <param name="allowPerms">The 'allow' access mask (hex string).</param>
/// <param name="denyPerms">The 'deny' access mask (hex string).</param>
private static void InsertGroupAceXML(XmlNamespaceManager xmlnsManager,
XmlDocument doc,
string acesXPath,
SecurityIdentifier sid,
string allowPerms,
string denyPerms)
{
XmlDocument acesDoc = new XmlDocument();
acesDoc.LoadXml(GetACEXml(sid, allowPerms, denyPerms));
XmlNode allowNode = acesDoc.SelectSingleNode("//S:access_allowed_ace", xmlnsManager);
XmlNode denyNode = acesDoc.SelectSingleNode("//S:access_denied_ace", xmlnsManager);
XmlNode acesNode = doc.DocumentElement.SelectSingleNode(acesXPath, xmlnsManager);
// Check to see if the last node is for "Everyone" - well known SID S-1-1-0 - ace for a group needs
// to be inserted before it.
XmlNode refNode = acesNode.LastChild;
XmlNode sidNode = refNode.SelectSingleNode("S:sid/S:string_sid", xmlnsManager);
if (sidNode.InnerText == "S-1-1-0")
{
refNode = refNode.PreviousSibling;
}
refNode = acesNode.InsertAfter(doc.ImportNode(denyNode, true), refNode);
while (refNode.PreviousSibling != null)
{
refNode = refNode.PreviousSibling;
if (refNode.Name == "S:access_denied_ace")
{
sidNode = refNode.SelectSingleNode("S:sid/S:string_sid", xmlnsManager);
if (!ActiveDirectoryService.IsGroup(sidNode.InnerText))
{
break;
}
}
else
{
break;
}
}
acesNode.InsertAfter(doc.ImportNode(allowNode, true), refNode);
}
/// <summary>
/// Generates the ACE xml to be added to a WebDAV query to specify the permissions for a particular SID.
/// It is prettyy difficult finding documentation on what the structure of the xml should be for these
/// queries and even more difficult to find out what the permissions masks should be. The simplest way
/// I have found to determine both of these is to use an application called PFDAVAdmin which at the time
/// of writing could be downloaded from:
/// http://www.microsoft.com/downloads/details.aspx?FamilyID=635be792-d8ad-49e3-ada4-e2422c0ab424&DisplayLang=en
/// </summary>
/// <param name="sid">The SID to specify the permissions for.</param>
/// <param name="allowPerms">A HEX mask of the permissions to allow.</param>
/// <param name="denyPerms">A HEX mask of the permissions to deny.</param>
/// <returns>The XML.</returns>
private static string GetACEXml(SecurityIdentifier sid, string allowPerms, string denyPerms)
{
StringBuilder buf = new StringBuilder();
buf.Append("<?xml version=\"1.0\"?>");
buf.Append("<add xmlns:S=\"http://schemas.microsoft.com/security/\">");
buf.Append("<S:access_allowed_ace S:inherited=\"0\">");
buf.Append("<S:access_mask>");
buf.Append(allowPerms);
buf.Append("</S:access_mask>");
buf.Append("<S:sid>");
buf.Append("<S:string_sid>");
buf.Append(sid.Value);
buf.Append("</S:string_sid>");
buf.Append("</S:sid>");
buf.Append("</S:access_allowed_ace>");
buf.Append("<S:access_denied_ace S:inherited=\"0\">");
buf.Append("<S:access_mask>");
buf.Append(denyPerms);
buf.Append("</S:access_mask>");
buf.Append("<S:sid>");
buf.Append("<S:string_sid>");
buf.Append(sid.Value);
buf.Append("</S:string_sid>");
buf.Append("</S:sid>");
buf.Append("</S:access_denied_ace>");
buf.Append("</add>");
return buf.ToString();
}
/// <summary>
/// Removes some things in the XML that aren't required/wanted.
/// </summary>
/// <param name="xmlnsManager">Namespace mappings required when performing XPath queries.</param>
/// <param name="doc">The document that is being updated.</param>
private static void TidyPropUpdate(XmlNamespaceManager xmlnsManager,
XmlDocument doc)
{
XmlNode secDescr = doc.DocumentElement.SelectSingleNode("//S:security_descriptor", xmlnsManager);
if (secDescr != null)
{
XmlAttribute mapiAttr = (XmlAttribute)secDescr.Attributes.GetNamedItem("S:from_mapi_tlh");
secDescr.Attributes.Remove(mapiAttr);
}
// This is *key*!!! Without it problems arise when trying to update a non-email enabled folder
XmlNode protectedNode = doc.DocumentElement.SelectSingleNode("//@S:protected", xmlnsManager);
if (protectedNode != null)
{
protectedNode.Value = "1";
}
XmlNode autoInheritNode = doc.DocumentElement.SelectSingleNode("//@S:autoinherited", xmlnsManager);
if (autoInheritNode != null)
{
autoInheritNode.Value = "0";
}
foreach (XmlNode node in doc.DocumentElement.SelectNodes("//@S:inherited", xmlnsManager))
{
node.Value = "0";
}
XmlNode revisionNode = doc.DocumentElement.SelectSingleNode("//S:revision", xmlnsManager);
revisionNode.ParentNode.RemoveChild(revisionNode);
}