Migrate AD Group Permissions in SharePoint

A client recently deployed a brand new AD environment and started migrating users over to the new forest. It was decided that servers will remain in the old forest for a bit, and that coexistence will be in effect long enough to ensure a smooth transition.

One of the servers “left behind” was a SharePoint box (or more precisely, WSS 2.0 on Windows Server 2003). The client wanted to update all AD users and groups with permissions in SharePoint to match the target domain. To do this with user accounts, they ran STSADM.exe MIGRATEUSER command. Unfortunately that command does not process AD security groups, so they asked for a script or another solution to achieve this.

The client also had no proper test environment and wanted to be able to perform a “what if” dry run first, pointing out all the places where changes would be made, and then execute an actual migration on a limited set of resources (one of the many sites) to confirm that things are working well – only then they would be prepared to perform the actual migration of the entire SharePoint platform.

I was essentially asked to “replace the old domain name anywhere you see it” – presumably in ACLs on sites, lists, folders, and items, as well as in Sharepoint groups. So my challenge is to write something that will simply go through it all and magically add target domain security groups to the same non-inherited ACLs where source domain security groups are used.

SharePoint API to the Rescue

SharePoint is not my favorite product to work with but the good news is that it has a .NET API – so the answer to the client is “yes, possible”. Then I go and figure out “how”.

A few quick searches on Google reveal the following:

  • Migrate Users / Groups via PowerShell script – this MSDN post describes how to use MigrateUserAccount and MigrateGroup methods of the farm object
  • STSADM.exe with MigrateUser apparently calls MigrateUserAccount method so calling MigrateGroup in your own code is essentially equal to running STSADM but target groups
  • The PowerShell script solution in this post is simple and elegant, except I don’t have that kind of access to the client’s environment to be able to produce a CSV input file.
  • Due to the client requirements, we could not use this PowerShell script but the basic idea is there. Also, it appears that this script does not apply to Windows Server 2003 / WSS 2.0.

Hooking into SharePoint API

SharePoint managed code API is implemented in Microsoft.Sharepoint.dll and associated DLLs in C:\Program Files\Common Files\Microsoft\Shared\Web Extensions\12\ISAPI folder. Your .NET project needs to reference this assembly and then import Microsoft.Sharepoint namespace.

SharePoint Object Model

Next, I had to get up to speed on the structure of SharePoint object collections, objects, and ways of getting at them. The following MSDN resources came in handy:

In a nutshell, SharePoint objects are organized as Sites – Webs – Lists. Site is something you connect to, to enumerate all webs. Webs may contain lists. Lists may contain collections of list items, which can be folders or documents, nested in a random fashion (folders can contain other folders or documents, child folders can contain other subfolders, etc).

Each type of object has its own ACL associated with it, that contains a list of Role Assignments. Role Assignments link Role Definitions with Security Principals, such as AD user or AD group, or a SharePoint group. This is the critical piece of information we will exploit in the code below.

Last piece of knowledge we need before diving in, is that ACLs can be inherited from the parent, or be unique (as in, what happens when inheritance is broken). Inherited permissions are of no interest to us since they will inherit permission changes we will make at the parent container level; also, if you attempt to modify inherited permissions without breaking inheritance, the code will bomb.

SharePoint AD Group Migration – Code

So, the code (and yes it is not broken up into functions and subroutines very well and could use error handling):

Imports Microsoft.SharePoint

Module Module1

    Sub Main()

        Dim site As New SPSite(My.Settings.siteURL)
        site.OpenWeb()

        Dim webCol As SPWebCollection = site.AllWebs
        Dim web As SPWeb
        Dim webroleassignments As List(Of SPRoleAssignment)

        For Each web In webCol
            Console.WriteLine(vbCrLf & "\\\\\\ site: " & web.Name & "; Inheritance broken: " & web.HasUniqueRoleAssignments.ToString)
            webroleassignments = New List(Of SPRoleAssignment)

            For Each role As SPRoleAssignment In web.RoleAssignments
                If isSharepointGroup(role) Then
                    If web.HasUniqueRoleAssignments Then MigrateADGroupsInSharepointGroup(role)
                Else
                    Dim principal As SPPrincipal = role.Member
                    Dim roleuser As SPUser = CType(principal, SPUser)
                    Dim binds As SPRoleDefinitionBindingCollection = role.RoleDefinitionBindings
                    For Each bind As SPRoleDefinition In binds
                        If web.HasUniqueRoleAssignments = False Then GoTo nextwebbind
                        If roleuser.IsDomainGroup = False Then GoTo nextwebbind
                        If InStr(bind.Name, "Limited Access") > 0 Then GoTo nextwebbind
                        If InStr(roleuser.LoginName, My.Settings.olddomain) = 0 Then GoTo nextwebbind
                        Try
                            Dim newroleass As New SPRoleAssignment(web.EnsureUser(SwapGroups(roleuser.LoginName)))
                            newroleass.RoleDefinitionBindings.Add(bind)
                            webroleassignments.Add(newroleass)
                        Catch ex As SPException
                            Console.WriteLine("ERROR: " & ex.Message & " :: while adding " & SwapGroups(roleuser.LoginName) & " to " & web.Name)
                        End Try
nextwebbind:
                    Next bind
                End If
            Next role
            If My.Settings.makechanges And webroleassignments.Count > 0 Then UpdateWeb(web, webroleassignments)

            For Each list As SPList In web.Lists
                Console.WriteLine(vbCrLf & "list: " & list.Title & "; Inheritance broken: " & list.HasUniqueRoleAssignments.ToString)
                webroleassignments = New List(Of SPRoleAssignment)

                For Each role As SPRoleAssignment In list.RoleAssignments
                    If isSharepointGroup(role) Then
                        If list.HasUniqueRoleAssignments Then MigrateADGroupsInSharepointGroup(role)
                    Else
                        Dim principal As SPPrincipal = role.Member
                        Dim roleuser As SPUser = CType(principal, SPUser)
                        Dim binds As SPRoleDefinitionBindingCollection = role.RoleDefinitionBindings
                        For Each bind As SPRoleDefinition In binds
                            If list.HasUniqueRoleAssignments = False Then GoTo nextlistbind
                            If roleuser.IsDomainGroup = False Then GoTo nextlistbind
                            If InStr(bind.Name, "Limited Access") > 0 Then GoTo nextlistbind
                            If InStr(roleuser.LoginName, My.Settings.olddomain) = 0 Then GoTo nextlistbind
                            Try
                                Dim newroleass As New SPRoleAssignment(web.EnsureUser(SwapGroups(roleuser.LoginName)))
                                newroleass.RoleDefinitionBindings.Add(bind) 'roledef)
                                webroleassignments.Add(newroleass)
                            Catch ex As SPException
                                Console.WriteLine("ERROR: " & ex.Message & " :: while adding " & SwapGroups(roleuser.LoginName) & " to " & web.Name)
                            End Try
nextlistbind:
                        Next bind
                    End If
                Next role
                If My.Settings.makechanges And webroleassignments.Count > 0 Then Updatelist(web, list, webroleassignments)

                Dim query As New SPQuery()
                query.ViewAttributes = "Scope=""Recursive"""
                Dim myItems As SPListItemCollection = list.GetItems(query)
                Dim folder As SPListItem

                For Each folder In myItems
                    Console.WriteLine(vbTab & "folder/doc: " & folder.Name & "; Inheritance broken: " & folder.HasUniqueRoleAssignments.ToString)
                    webroleassignments = New List(Of SPRoleAssignment)

                    For Each role As SPRoleAssignment In folder.RoleAssignments
                        If isSharepointGroup(role) Then
                            If folder.HasUniqueRoleAssignments Then MigrateADGroupsInSharepointGroup(role)
                        Else
                            Dim principal As SPPrincipal = role.Member
                            Dim roleuser As SPUser = CType(principal, SPUser)
                            Dim binds As SPRoleDefinitionBindingCollection = role.RoleDefinitionBindings
                            For Each bind As SPRoleDefinition In binds
                                If folder.HasUniqueRoleAssignments = False Then GoTo nextitembind
                                If roleuser.IsDomainGroup = False Then GoTo nextitembind
                                If InStr(bind.Name, "Limited Access") > 0 Then GoTo nextitembind
                                If InStr(roleuser.LoginName, My.Settings.olddomain) = 0 Then GoTo nextitembind
                                Try
                                    Dim newroleass As New SPRoleAssignment(web.EnsureUser(SwapGroups(roleuser.LoginName)))
                                    newroleass.RoleDefinitionBindings.Add(bind)
                                    webroleassignments.Add(newroleass)
                                Catch ex As SPException
                                    Console.WriteLine("ERROR: " & ex.Message & " :: while adding " & SwapGroups(roleuser.LoginName) & " to " & web.Name)
                                End Try
nextitembind:
                            Next bind

                        End If
                    Next role
                    If My.Settings.makechanges And webroleassignments.Count > 0 Then UpdateFolder(web, folder, webroleassignments)

                Next folder

            Next list
nextweb:
        Next web

    End Sub

 

Key points in the code are:

  • Lines 7-8 connect to the main SharePoint site, http://localhost (this comes from the project settings)
  • Line 10 gets the list of all webs in the site we are connected to
  • Lines 14-41, this is the first block of code that enumerates through permissions assigned to the first web and builds a list of new role assignments that need to be added (webroleassignments, line 33). This block of code loops on line 111.
    • Lines 43-70 loop through collections of lists of each web, and essentially do the same as lines 14-41 but at the list level
      • Lines 72-105 loop through collections of folder and document items of each list, and do the same thing with permissions but at the item level
      • Lines 72-74 perform a recursive query that pulls in all folders and documents of a given list. This is a way to enumerate through all list items no matter how deeply and randomly nested

In the blocks of code that examine permissions, we do this:

  1. Enumerate through all role assignments
    1. Look into every SPGroup (= Sharepoint Group), detect any AD user/group role assignments
    2. Look into every SPUser (= directly assigned AD user or AD group)
    3. Enumerate through all security bindings for each security principal that a) is not inherited from the parent container, b) is a domain group, c) contains the domain name of the old AD domain and d) is not of type “Limited Access”
      1. There are two types of security bindings; role definitions (“Contributor”, “Guest”, “Full Access”, etc – predefined roles) and base permissions (“Add List Items”, etc). For best results, copy entire binding without digging into what it assigns, just make sure that it is assigned to the new domain group
    4. Matching entries are added to a collection where domain name is substituted and collection is sent to another function, which actually modifies and saves the ACL in question

“Limited Access” binding is assigned by SharePoint only and cannot be assigned programmatically or manually by humans. Limited Access gets assigned to parents of children which are granted other types of access permissions directly (not through inheritance). Limited Access grants ability to principals to traverse parent folders where they have no explicit or inherited permissions. Skip all instances of Limited Access – SharePoint will assign this automatically.

This code structure is somewhat complicated, but it allows you to control every aspect of permissions migration, filtering or adding anything you want while analyzing what’s in there already.

Functions That Do Actual Work

What we’ve done so far is analyzed which principals need to be converted from the old domain format to the new domain format, and where specifically that needs to be done in the site/web/list/folder/item structure. Once RoleAssignment collection is built, we call out to functions that modify ACLs. This cannot be done inside the code blocks that perform enumeration, as modifying the collection that is being enumerated immediately breaks enumeration.

    Private Sub MigrateADGroupsInSharepointGroup(ByVal role As SPRoleAssignment)

        Dim principal As SPPrincipal = role.Member
        Dim rolegroup As SPGroup = CType(principal, SPGroup)
        Dim list As New List(Of SPUser)

        For Each roleuser As SPUser In rolegroup.Users
            If Not IsNothing(SwapGroups(roleuser.LoginName)) And roleuser.IsDomainGroup Then list.Add(roleuser)
        Next

        For Each s As SPUser In list
            Console.WriteLine("Converting domain group " & s.LoginName & " to " & SwapGroups(s.LoginName))
            Try
                If My.Settings.makechanges Then rolegroup.Users.Add(SwapGroups(s.LoginName), s.Email, SwapGroups(s.LoginName), "(migrated) " & s.Notes)
            Catch ex As SPException
                Console.WriteLine("ERROR: " & ex.Message & " :: while adding " & SwapGroups(s.LoginName) & " to " & rolegroup.Name)
            End Try
        Next

        Console.WriteLine("Saving role " & role.Member.Name)
        If My.Settings.makechanges Then rolegroup.Update()

    End Sub

    Private Function SwapGroups(ByVal group As String) As String

        If InStr(group, My.Settings.olddomain) > 0 Then
            SwapGroups = My.Settings.newdomain & "\" & Split(group, "\").GetValue(1)
        Else
            SwapGroups = Nothing
        End If

    End Function

    Private Function isSharepointGroup(ByVal role As SPRoleAssignment) As Boolean

        Try
            If InStr(role.Member.GetType.ToString, "SPUser") > 0 Then
                Dim principal As SPPrincipal = role.Member
                Dim roleuser As SPUser = CType(principal, SPUser)
                isSharepointGroup = False
            Else
                Dim principal As SPPrincipal = role.Member
                Dim rolegroup As SPGroup = CType(principal, SPGroup)
                Dim principals As String = ""
                For Each roleuser As SPUser In rolegroup.Users
                    principals = principals + roleuser.LoginName + "; "
                Next
                isSharepointGroup = True
            End If

        Catch ex As Exception
            Console.WriteLine(ex.Message & vbTab & ex.InnerException.ToString)
            Return Nothing
        End Try

    End Function

    Private Sub UpdateWeb(ByVal web As SPWeb, ByVal roleassignments As List(Of SPRoleAssignment))

        Using site As New SPSite(My.Settings.siteURL)
            site.OpenWeb()
            Dim myweb As SPWeb = site.OpenWeb(web.ID)

            For Each r As SPRoleAssignment In roleassignments
                Console.WriteLine("===> adding " & r.Member.Name & " to web " & web.Name)
                myweb.RoleAssignments.Add(r)
            Next
            Console.WriteLine("=> saving " & myweb.Name)
            myweb.Update()

        End Using

    End Sub

    Private Sub Updatelist(ByVal web As SPWeb, ByVal list As SPList, ByVal roleassignments As List(Of SPRoleAssignment))

        Using site As New SPSite(My.Settings.siteURL)
            site.OpenWeb()
            Dim myweb As SPWeb = site.OpenWeb(web.ID)

            Try
                Console.WriteLine("Fetching list " & list.Title & " for update; ID " & list.ID.ToString)
                Dim mylist As SPList = myweb.GetListFromUrl(list.DefaultViewUrl)

                Console.WriteLine("Got list " & list.DefaultViewUrl)
                For Each r As SPRoleAssignment In roleassignments
                    Console.WriteLine("===> adding " & r.Member.Name & " to list " & mylist.Title)
                    mylist.RoleAssignments.Add(r)
                Next
                Console.WriteLine("=> saving " & mylist.Title)
                mylist.Update()

            Catch ex As Exception
                Console.WriteLine("ERROR: " & ex.Message & vbCrLf & ex.InnerException.ToString)
            End Try

        End Using

    End Sub

    Private Sub UpdateFolder(ByVal web As SPWeb, ByVal folder As SPListItem, ByVal roleassignments As List(Of SPRoleAssignment))

        Using site As New SPSite(My.Settings.siteURL)
            site.OpenWeb()
            Dim myweb As SPWeb = site.OpenWeb(web.ID)
            Dim myfolder As SPFolder = myweb.GetFolder(folder.Url)
            For Each r As SPRoleAssignment In roleassignments
                Console.WriteLine("===> adding " & r.Member.Name & " to folder " & myfolder.Name)
                myfolder.Item.RoleAssignments.Add(r)
            Next
            Console.WriteLine("=> saving " & myfolder.Name)
            myfolder.Update()

        End Using

    End Sub

End Module

 

These functions are pretty self-explanatory and the fact that I had to break them out only makes the rest of the code look a little more readable.

For the code to work, your source and target AD domains need to be reachable by the SharePoint server. The code will need to run locally on the SharePoint server.

You can download the VB.NET project source code from the Downloads page, feel free to modify, add whatever logic/conditions/logging to your migration routine, and redistribute as needed.

 

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *