I got a comment the other day on my blog about an issue with the copying/importing of lists. The problem was that the security settings on the list were not being preserved during the import. The natural assumption is that if I specify -includeusersecurity that any permissions on the list would be preserved. In other words, if my source list was not inheriting permissions on the list itself, a folder, or an individual item, then those custom permissions should carry over when I import the list to a new web. Unfortunately this is not the case – includeusersecurity only copies over the users and groups, not the permissions (at least when importing a list).

I tore through the code to see if there was some setting that I was missing that would make this work but alas, I found nothing. For me this problem was a huge concern because it would be too easy for someone to want to copy a list and just expect that the security remained the same (as I unfortunately did). Because of the potential security issues I decided it was prudent to find a workaround. What I ended up creating was a new command which could be used to copy the permissions from one list to another: gl-copylistsecurity.

The code is also written so that it can be used independently as its own command or via the existing gl-importlist and gl-copylist commands, which have now been updated to utilize this new feature. By default the command, when used independently, will only copy the permissions for the list itself and all folders – it will not copy permissions for individual list items unless you pass in the -includeitemsecurity parameter. When used by the gl-copylist command and the gl-importlist command this option is set to true. This way you can set the security on one list the way you want it and then propagate those settings to other, similarly structured lists without having to do it by hand. The code can be seen below:

  1/// <summary>
  2/// Runs the specified command.
  3/// </summary>
  4/// <param name="command">The command.</param>
  5/// <param name="keyValues">The key values.</param>
  6/// <param name="output">The output.</param>
  7/// <returns></returns>
  8public override int Run(string command, System.Collections.Specialized.StringDictionary keyValues, out string output)
  9{
 10    output = string.Empty;
 11
 12    InitParameters(keyValues);
 13
 14    string sourceUrl = Params["sourceurl"].Value;
 15    string targetUrl = Params["targeturl"].Value;
 16    bool quiet = Params["quiet"].UserTypedIn;
 17    bool includeItemSecurity = Params["includeitemsecurity"].UserTypedIn;
 18
 19    using (SPSite sourceSite = new SPSite(sourceUrl))
 20    using (SPSite targetSite = new SPSite(targetUrl))
 21    using (SPWeb sourceWeb = sourceSite.OpenWeb())
 22    using (SPWeb targetWeb = targetSite.OpenWeb())
 23    {
 24        SPList sourceList = Utilities.GetListFromViewUrl(sourceWeb, sourceUrl);
 25        SPList targetList = Utilities.GetListFromViewUrl(targetWeb, targetUrl);
 26
 27        if (sourceList == null)
 28            throw new SPException("Source list was not found.");
 29        if (targetList == null)
 30            throw new SPException("Target list was not found.");
 31
 32        CopySecurity(sourceList, targetList, targetWeb, includeItemSecurity, quiet);
 33    }
 34    return 1;
 35}
 36 
 37 
 38 
 39/// <summary>
 40/// Copies the security.
 41/// </summary>
 42/// <param name="sourceList">The source list.</param>
 43/// <param name="targetList">The target list.</param>
 44/// <param name="targetWeb">The target web.</param>
 45/// <param name="includeItemSecurity">if set to <c>true</c> [include item security].</param>
 46/// <param name="quiet">if set to <c>true</c> [quiet].</param>
 47internal static void CopySecurity(SPList sourceList, SPList targetList, SPWeb targetWeb, bool includeItemSecurity, bool quiet)
 48{
 49    if (!quiet)
 50        Console.WriteLine("Start Time: {0}.", DateTime.Now);
 51
 52    try
 53    {
 54        // Set the security on the list itself.
 55        SetObjectSecurity(targetWeb, sourceList, targetList, quiet, targetList.RootFolder.ServerRelativeUrl);
 56
 57        // Set the security on any folders in the list.
 58        SetFolderSecurity(targetWeb, sourceList, targetList, quiet);
 59
 60        if (includeItemSecurity)
 61        {
 62            // Set the security on list items.
 63            SetListItemSecurity(targetWeb, sourceList, targetList, quiet);
 64        }
 65    }
 66    finally
 67    {
 68        if (!quiet)
 69            Console.WriteLine("Finish Time: {0}.\r\n", DateTime.Now);
 70    }
 71}
 72 
 73/// <summary>
 74/// Sets the list item security.
 75/// </summary>
 76/// <param name="targetWeb">The target web.</param>
 77/// <param name="sourceList">The source list.</param>
 78/// <param name="targetList">The target list.</param>
 79/// <param name="quiet">if set to <c>true</c> [quiet].</param>
 80private static void SetListItemSecurity(SPWeb targetWeb, SPList sourceList, SPList targetList, bool quiet)
 81{
 82    foreach (SPListItem sourceItem in sourceList.Items)
 83    {
 84        SPListItem targetItem = null;
 85        foreach (SPListItem i in targetList.Items)
 86        {
 87            if (sourceItem.Name == i.Name)
 88            {
 89                targetItem = i;
 90                break;
 91            }
 92        }
 93        if (targetItem == null)
 94            continue;
 95
 96        SetObjectSecurity(targetWeb, sourceItem, targetItem, quiet, sourceItem.Name);
 97    }
 98}
 99 
100/// <summary>
101/// Sets the folder security.
102/// </summary>
103/// <param name="targetWeb">The target web.</param>
104/// <param name="sourceList">The source list.</param>
105/// <param name="targetList">The target list.</param>
106/// <param name="quiet">if set to <c>true</c> [quiet].</param>
107private static void SetFolderSecurity(SPWeb targetWeb, SPList sourceList, SPList targetList, bool quiet)
108{
109    foreach (SPListItem sourceFolder in sourceList.Folders)
110    {
111        SPListItem targetFolder = null;
112        foreach (SPListItem f in targetList.Folders)
113        {
114            if (f.Folder.ServerRelativeUrl.Substring(targetList.RootFolder.ServerRelativeUrl.Length) ==
115                sourceFolder.Folder.ServerRelativeUrl.Substring(sourceList.RootFolder.ServerRelativeUrl.Length))
116            {
117                targetFolder = f;
118                break;
119            }
120        }
121        if (targetFolder == null)
122            continue;
123
124        SetObjectSecurity(targetWeb, sourceFolder, targetFolder, quiet, targetFolder.Folder.ServerRelativeUrl);
125    }
126}
127 
128/// <summary>
129/// Sets the object security.
130/// </summary>
131/// <param name="targetWeb">The target web.</param>
132/// <param name="sourceObject">The source object.</param>
133/// <param name="targetObject">The target object.</param>
134/// <param name="quiet">if set to <c>true</c> [quiet].</param>
135/// <param name="itemName">Name of the item.</param>
136private static void SetObjectSecurity(SPWeb targetWeb, ISecurableObject sourceObject, ISecurableObject targetObject, bool quiet, string itemName)
137{
138    if (!sourceObject.HasUniqueRoleAssignments && targetObject.HasUniqueRoleAssignments)
139    {
140        if (!quiet)
141            Console.WriteLine("Progress: Setting target object to inherit permissions from parent for \"{0}\".", itemName);
142        targetObject.ResetRoleInheritance();
143        return;
144    }
145    else if (sourceObject.HasUniqueRoleAssignments && !targetObject.HasUniqueRoleAssignments)
146    {
147        if (!quiet)
148            Console.WriteLine("Progress: Breaking target object inheritance from parent for \"{0}\".", itemName);
149        targetObject.BreakRoleInheritance(false);
150    }
151    else if (!sourceObject.HasUniqueRoleAssignments && !targetObject.HasUniqueRoleAssignments)
152    {
153        if (!quiet)
154            Console.WriteLine("Progress: Ignoring \"{0}\".  Target object and source object both inheritance from parent.", itemName);
155        return; // Both are inheriting so don't change.
156    }
157
158    foreach (SPRoleAssignment ra in sourceObject.RoleAssignments)
159    {
160        SPRoleAssignment existingRoleAssignment = GetRoleAssignement(targetObject, ra.Member);
161        if (existingRoleAssignment == null)
162        {
163            targetObject.RoleAssignments.Add(ra);
164
165            existingRoleAssignment = GetRoleAssignement(targetObject, ra.Member);
166            if (existingRoleAssignment != null)
167            {
168                if (!quiet)
169                    Console.WriteLine("Progress: Added \"{0}\" to target object \"{1}\".", ra.Member, itemName);
170                continue;
171            }
172
173            SPRoleAssignment newRA = new SPRoleAssignment(ra.Member);
174            foreach (SPRoleDefinition rd in ra.RoleDefinitionBindings)
175            {
176                if (rd.Name == "Limited Access")
177                    continue;
178
179                SPRoleDefinition existingRoleDef = targetWeb.RoleDefinitions[rd.Name];
180                if (existingRoleDef == null)
181                {
182                    existingRoleDef = new SPRoleDefinition();
183                    existingRoleDef.BasePermissions = rd.BasePermissions;
184                    existingRoleDef.Description = rd.Description;
185                    existingRoleDef.Update();
186                    targetWeb.RoleDefinitions.Add(existingRoleDef);
187                }
188                newRA.RoleDefinitionBindings.Add(existingRoleDef);
189            }
190            if (newRA.RoleDefinitionBindings.Count == 0)
191            {
192                if (!quiet)
193                    Console.WriteLine("Progress: Unable to add \"{0}\" to target object \"{1}\" (principals with only \"Limited Access\" cannot be added).", ra.Member, itemName);
194                continue;
195            }
196
197            targetObject.RoleAssignments.Add(newRA);
198
199            existingRoleAssignment = GetRoleAssignement(targetObject, ra.Member);
200            if (existingRoleAssignment == null)
201            {
202                Console.WriteLine("Progress: Unable to add \"{0}\" to target object \"{1}\".", ra.Member, itemName);
203            }
204            else
205            {
206                if (!quiet)
207                    Console.WriteLine("Progress: Added \"{0}\" to target object \"{1}\".", ra.Member, itemName);
208            }
209        }
210    }
211}
212 
213/// <summary>
214/// Gets the role assignement.
215/// </summary>
216/// <param name="securableObject">The securable object.</param>
217/// <param name="principal">The principal.</param>
218/// <returns></returns>
219private static SPRoleAssignment GetRoleAssignement(ISecurableObject securableObject, SPPrincipal principal)
220{
221    SPRoleAssignment ra = null;
222    try
223    {
224        ra = securableObject.RoleAssignments.GetAssignmentByPrincipal(principal);
225    }
226    catch (ArgumentException)
227    { }
228    return ra;
229}

One thing to note – I’ve not had a lot of time to test every possible scenario with this. It’s quite possible you may run into issues if a Role Assignment can’t be found due to a missing user or group or something like that (though I believe from my testing that it should simply ignore those users and not add the permission – I don’t believe it will try to add the user or group so make sure all your users of interest exist in the target site). It worked quite well for me in my environment but I’m running short on time and was not able to test a lot of scenarios. If you run into any issues please pass your findings along so that others can benefit. The syntax of the command can be seen below:

C:\>stsadm -help gl-copylistsecurity

stsadm -o gl-copylistsecurity

Copies a list's security settings from one list to another.

Parameters:
        -sourceurl <list view url to copy security from>
        -targeturl <list view url to copy security to>
        [-quiet]
        [-includeitemsecurity]

Here’s an example of how to copy the permissios from one list to another:

stsadm -o gl-copylistsecurity -sourceurl "http://intranet/documents/forms/allitems.aspx" -targeturl "http://intranet/testweb1/documents/forms/allitems.aspx" -includeitemsecurity

Note that gl-copylist and gl-importlist have been updated to include this functionality.