Automate Creation of Azure AD Application with OAuth Permissions

<Update 2018-02-06>Updated with snippet to list out GUIDs for app roles that can be assigned.</Update>

In this post I will show how to automate the creation of an Azure AD Application and assign OAuth permissions to that application.  The latter part is tricky as there is not currently a PowerShell commandlet or Azure CLI command to assign OAuth permissions.  Instead we will leverage an authenticated call to the Microsoft Graph to assign the permissions.  For more in depth information about Azure AD apps, verifying the results, and more please see the following post which I am borrowing heavily from.  I had difficulty finding this information so this post is my attempt to spread the word and also add a few clarifications on the ADAL libraries used.

(Read this first!) Automating the creation of Azure AD Applications by Christer Ljung

http://www.redbaronofazure.com/?p=7197

Problem

Creating Azure AD apps typically involves logging into the Azure Portal (classic or “new” / Ibiza version) and manually clicking through multiple screens.  When developing a solution that needs to leverage Office 365 services (as is my case with a current project) it is helpful to automate the process of creating the Azure AD app and assigning the permissions.  If you happen to be assigning Admin permissions then additional steps will be required by an Azure AD domain administrator (see following screenshot).

AzureADApp1

Solution

Creating an Azure AD application can be accomplished in 2 lines of PowerShell.  Login to Azure then create the app.


Login-AzureRmAccount

$aadapp = New-AzureRmADApplication -DisplayName "Some amazing app" -HomePage https://localhost:8081/ -IdentifierUris https://localhost:8081/

***BONUS***

If you want to create an app that uses certificate based authentication you can use the following PowerShell commandlets.

Note: The commandlets for creating and exporting a certificate require Windows 8 or higher.  There are workarounds for Windows 7 or similar OS.  Feel free to reach out if you are in that scenario.


$pwd = Read-Host -AsSecureString -Prompt "Enter certificate password"

# process for Windows 8+ type OS
$ssc = New-SelfSignedCertificate -CertStoreLocation cert:\localmachine\my -Provider "Microsoft Enhanced RSA and AES Cryptographic Provider" `
                           -Subject "cn=MySuperSpecialCert" -KeyDescription "Used to access Azure Resources" `
                           -NotBefore (Get-Date).AddDays(-1) -NotAfter (Get-Date).AddYears(1)

# Export cert to PFX - uploaded to Azure App Service
Export-PfxCertificate -cert cert:\localMachine\my\$($ssc.Thumbprint) -FilePath ExportedSpecialCertFile.pfx -Password $pwd –Force
$KeyStorageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet
$certFile = Get-ChildItem –Path <path to certificate file>
$x509 = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
$x509.Import($certFile.FullName, $pwd, $KeyStorageFlags)
$certValue = [System.Convert]::ToBase64String($x509.GetRawCertData())

# should match our certificate entries above.
$validFrom = [System.DateTime]::Now.AddDays(-1)
$validTo = [System.DateTime]::Now.AddYears(1)

$aadapp = New-AzureRmADApplication –DisplayName "Some amazing app" -HomePage "https://localhost:8080/" `
                                   -IdentifierUris "https://localhost:8080/" -CertValue $certValue `
                                   -StartDate $validFrom -EndDate $validTo

The next step involves granting OAuth permissions to the recently created Azure AD app.  As of the writing of this blog (Feb 2, 2018) there is not a PowerShell commandlet nor Azure CLI command to assign those permissions.  There is however a way to use the Microsoft Graph to assign permissions.  This is an adapted version of Christer’s example that I referenced earlier and uses a local version of the Active Directory Authentication Library (ADAL) DLLs.  Currently these are at version 3.19.1.

ADAL NuGet package

https://www.nuget.org/packages/Microsoft.IdentityModel.Clients.ActiveDirectory/

Extract the following DLLs into the folder where you are executing other PowerShell commands:

  • Microsoft.IdentityModel.Clients.ActiveDirectory.dll
  • Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll

$Tenant = "<Office 365 tenant name, ex. Contoso>"
$aadTenant = "$Tenant.onmicrosoft.com"
$adminUser = "<admin account with access to authenticate against MS Graph>"

# load ADAL DLLs
$adal = ".\Microsoft.IdentityModel.Clients.ActiveDirectory.dll"
$adalforms = ".\Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll"

[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null
[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null

 $clientId = "1950a258-227b-4e31-a9cf-717495945fc2"  # Set well-known client ID for AzurePowerShell
  $redirectUri = "urn:ietf:wg:oauth:2.0:oob" # Set redirect URI for Azure PowerShell
  $resourceAppIdURI = "https://graph.windows.net/" # resource we want to use
  $adminUserId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($adminUser, "OptionalDisplayableId")

 # Create Authentication Context tied to Azure AD Tenant
  $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority

  # Acquire token
  $authResult = $authContext.AcquireToken($resourceAppIdURI, $clientId, [Uri]$redirectUri, [Microsoft.IdentityModel.Clients.ActiveDirectory.PromptBehavior]::Always, $adminUserId)

 $authHeader = $authResult.CreateAuthorizationHeader()
  $headers = @{"Authorization" = $authHeader; "Content-Type"="application/json"}   

# make call against MS Graph to apply OAuth permissions
$url = "https://graph.windows.net/$aadTenant/applications/$($aadapp.ObjectID)?api-version=1.6"
$postData = "{'requiredResourceAccess':[
     {'resourceAppId':'00000003-0000-0ff1-ce00-000000000000','resourceAccess':[{'id':'fbcd29d2-fcca-4405-aded-518d457caae4','type':'Role'}]},
     {'resourceAppId':'00000002-0000-0000-c000-000000000000','resourceAccess':[{'id':'311a71cc-e848-46a1-bdf8-97ff7156d8e6','type':'Scope'}]}
     ]}";
$result = Invoke-RestMethod -Uri $url -Method "PATCH" -Headers $headers -Body $postData  

Note the use of specific resoureAppId and resourceAccess values above.  These two examples grant the “read and write all items in SharePoint Online” admin consent permission and the default “read user profile data” delegated permission respectively.  In order to find out the GUIDs you may need you’ll need to add the permissions through the Azure portal UI, check the manifest file, and extract the GUIDs.  See Christer’s post for more details.

<Update 2018-02-06>  I recently found out it is possible to list out the Application role permissions and GUIDs needed above by running the following PowerShell against the Azure AD module (I’m using Azure AD “V2” Preview module, haven’t verified against the existing V1 module).

Connect-AzureAD
# 00000003-0000-0ff1-ce00-000000000000 is the AppId for SharePoint Online, call Get-AzureADServicePrincipal by itself to find other AppIds
$SPOApi = Get-AzureADServicePrincipal -Filter "AppId eq '00000003-0000-0ff1-ce00-000000000000'"
$SPOApi.AppRoles

</Update>

If you happen to assign an admin consent permissions (such as the “read and write all items in SharePoint Online” permission) an Azure AD domain administrator will still need to consent to that permission by clicking “Grant permission” inside the Azure portal.  I’m not aware of a way to automate that process but if you do know please share in the comments below.

Conclusion

Originally I had hoped automating creation of an Azure AD app would be a simple process.  Creation of the Azure AD app is easy, but adding certificate authentication and / or assigning OAuth permissions adds extra work to be done.  As seen in this post though much of that can be automated.  Hopefully this post saves you time and effort.  Feel free to leave any feedback or questions in the comments below.

-Frog Out

Controlling Office 365 Admin Access with Azure AD Privileged Identity Management (PIM)

   Controlling, monitoring, and revoking access to privileged accounts can be a difficult process.  Recently my coworker Ken Kilty shared with me a new service for Azure Active Directory called Privileged Identity Management (Azure AD PIM).  After spending some time with it I wanted to share with a broader audience since I had never heard of it previously.

image

 

Overview

   Please read the What is Azure AD Privileged Identity Management first for a good overview of implementation, example scenario, and additional links to resources.  Note that Azure AD PIM requires Azure AD Premium P2 licenses.  If you would like to test this out there is a free 30 day trial of Azure AD Premium P2 for up to 100 users.

   Granting administrator access, for any application or server, to users should always be done with caution.  Sometimes what starts out as a temporary elevation of permissions turns into a permanent assignment.  Azure AD PIM answers many of the tough questions for Azure AD, Office 365, and related services such as:

  • Who has admin access to <service X>?
  • How do I grant truly temporary access to <service Y>?
  • How can I review all current admins to see if they still need admin access?

   The goal with Azure AD PIM is to allow administrators to define either permanent or “eligible” assignment of specific elevated permissions within Azure and Office 365.  Currently there are 21 roles that can be managed such as Global Administrator, Password Administrator, SharePoint Service Administrator, Exchange Administrator, and more.  See Assigning administrator roles in Azure Active Directory for a more complete listing of roles.  Users who are defined as “eligible” will be able to elevate themselves to roles they have been assigned for a set number of hours (1-72) defined by a Azure AD PIM administrator.  During this role elevation process the “eligible” user will need to verify their identity through a text / call verification or multifactor authentication (MFA) mechanism.  One of the key advantages is that this entire interaction is tracked and auditable.  Administrators can even require an incident or service ticket number prior to elevation and receive alerts when elevation requests are processed.

 

Conclusion

   I have seen privileged role access handled in many different ways at customers over the years.  Having a consistent and auditable process ensures that changes can be tracked and users who no longer need elevated permissions can be removed.  In the time I’ve tested out Azure AD Privileged Identity Management I am very happy with the overall process and review options.  One word of advice for users elevating yourself.  You will need to log out and log back in in order to update your claim token with the new elevated role claims.  Give Azure Active Directory Privileged Identity Management a try and share any feedback in the comments below.

 

      -Frog Out

My Experience Configuring Cloud Hybrid Search Service Application for SharePoint

   In this post I’ll talk through my personal experience deploying the new cloud hybrid search service application for SharePoint 2013 (also available in SharePoint 2016).  By no means am I an expert on this topic (especially in many of the supporting technologies such as AAD Connect, AD FS, etc.) but this is more meant to increase exposure to this new offering.  For an overview of cloud hybrid search and more information about actual implementation (which I will refer back to later) please read through Cloud Hybrid Search Service Application written by two of my Microsoft peers Neil and Manas (they are the true experts).

 

Components

   Here is a list of the high level components I used for my deployment.

Note: My Azure VM configuration is not using best practices for where or how to deploy different services.  Also my mention of GoDaddy and DigiCert are purely for example purposes and not an endorsement for either company.  I just happen to use their services and products in this scenario.

  • Office 365 (O365) trial tenant (sign up for one here)
  • 4 Azure VMs
    • A1 – Active Directory Domain Services (AD DS)
    • A1 – Active Directory Federation Services (AD FS)
    • A2 – Azure Active Directory Connect (AAD Connect), Web Application Proxy (WAP)
    • A4 – SQL Server 2014, SharePoint 2013 farm with Service Pack 1 and at least Aug 2015 CU
  • Custom domain (purchased through GoDaddy but any domain registrar should work)
    • Note: Office 365 does have a partnership with GoDaddy so configuration may be easier due to automated updates that can be performed
    • Additionally I was able to modify public DNS records through GoDaddy to allow federated authentication through AD FS
  • SSL wildcard certificate purchased from DigiCert
    • Only required if want to allow Office 365 user to open / preview a search result that resides on-prem with Office Online Server (new version of Office Web Apps Server 2013, not discussed in this post)
    • I also used this certificate for other purposes such as securing AD FS communication and implementing Remote Desktop Gateway (the latter is unrelated to this post)
  • Custom result source to display O365 search results in my on-prem farm

 

   Next we’ll take a look at some of these components more in depth.

 

SharePoint Server

   The new cloud hybrid search service application is available in SharePoint Server 2013 with the August 2015 CU or later.  I have heard from my peers that there are some issues with cloud hybrid search as of the October, November, and December 2015 CUs.  As such use either the August or September 2015 CUs at the time of this writing (Dec 8, 2015) or wait until the Jan 2016 CU which should contain the fix (link).  The SharePoint Server 2016 IT Preview 1 also supports cloud hybrid search although I have not tested it out myself.

 

Cloud Search Service Application

   To provision a cloud hybrid search service application the property CloudIndex on the service application must be set to True.  This property is a read-only property and can only be set at creation time.  As such you will need to create a new search service application in order to utilize the cloud hybrid search service.

   I have not tested creating a new cloud hybrid search service application using a restored backup admin database from an existing search service application.  The thought behind this would be to retain at least a portion of your existing search service application.  If you try this and have any findings let me know in the comments below.

 

Custom Domain

   A custom domain is not a requirement for cloud hybrid search.  I used one so that I could allow end users (demo accounts) to log into Office 365 as a federated user “someUser@<fakecompany>.com” rather than the default domain “someUser@<O365TenantDomain>.onmicrosoft.com”.

 

AAD Connect

   In order to search for on-prem content that has been indexed by Office 365 the user will need to have an account that is synchronized to Azure Active Directory / Office 365.  This allows the search service in Office 365 to show content based on the Access Control List (ACL) defined on-prem.

   There are multiple options available for synchronizing accounts between on-prem and AAD but the predominate ones include DirSync, AAD Sync, and AAD Connect.  Since AAD Connect is the future looking tool of choice of these three I decided to use it.  AAD Connect automates many of the tedious tasks of configuring federated authentication by stepping through a wizard.

   That said I did run into a number of issues during configuration due to missing certificates, invalid permissions, or other steps I missed or was unaware of.  If I got part of the way through the configuration and ran into a failure that I couldn’t recover from then I had to uninstall AAD Connect (do not remove all prerequisites when prompted), wipe out the contents of “<install drive>:Program FilesMicrosoft Azure AD SyncData”, and then re-install.

 

Display Search Results On-Prem

 

***PLEASE READ AS THIS IS IMPORTANT***

    The default scenario for cloud hybrid search is to index both on-prem and O365 content which are then queried in O365.  It is possible to create or modify an on-prem result source to use the remote index from your Office 365 tenant which allows for querying and display the combined search results on-prem.  The problem though is that when you query for and click results on-prem the search analytics click data is not incorporated back to the cloud index to further influence search results.

Ex. I queried for “SharePoint” in on-prem search center and clicked the 4th result on result page.  Multiple other users also searched for “SharePoint” and clicked the same 4th result.  SharePoint search (via timer jobs and other background processes) incorporates that click data and adjusts the 4th result to now appear higher in rankings upon subsequent search queries.

   I have unsuccessfully tested a few options to manually pass the search click data up to SharePoint Online.  These include creating a ClientContext object and calling the RecordPageClick() method on SearchExecutor, modifying the display template page, and more.  I did hear from a SharePoint MVP that successfully tested out a way to push search analytics data between on-prem and O365 but it took a fair amount of customizations to accomplish.  If I find out any additional information, workaround, or updates on this topic I’ll update this post to reflect that.

 

Example

   As you can see from the below screenshots I can initiate a search query from on-prem or O365 (respectively) and get the same combined result set.

 

OnPremResults

 

SPOResults

 

 

Conclusion

   Due to my prior inexperience around AD FS, Web Application Proxy, AAD Connect, and other applications it took me a few days to get everything working end-to-end.  After that small hurdle I was very excited to be seeing combined on-prem and O365 search results in both on-prem and O365.  Do note though the section above calling out the current issue with search analytics data not being sent back and forth.  Aside from that I am looking forward to testing this out with customers and reaping the many benefits such as inclusion of content in the Microsoft Graph (formerly Office Graph) / Delve and other O365 only offerings.

 

      -Frog Out

PowerShell Script to Enumerate SharePoint 2010 or 2013 Permissions and Active Directory Group Membership

   In this post I will present a script to enumerate SharePoint 2010 or 2013 permissions across the entire farm down to the site (SPWeb) level.  As a bonus this script also recursively expands the membership of any Active Directory (AD) group including nested groups which you wouldn’t be able to find through the SharePoint UI.

 

History

    Back in 2009 (over 4 years ago now) I published one my most read blog posts about enumerating SharePoint 2007 permissions.  I finally got around to updating that script to remove deprecated APIs, supporting the SharePoint 2010 commandlets, and fixing a few bugs.  There are 2 things that script did that I had to remove due to major architectural or procedural changes in the script.

  1. Indenting the XML output
  2. Ability to search for a specific user

   I plan to add back the ability to search for a specific user but wanted to get this version published first.  As for indenting the XML that could be added but would take some effort.  If there is user demand for it (let me know in the comments or email me using the contact button at top of blog) I’ll move it up in priorities.

   As a side note you may also notice that I’m not using the Active Directory commandlets.  This was a conscious decision since not all environments have them available.  Instead I’m relying on the older [ADSI] type accelerator and APIs.  It does add a significant amount of code to the script but it is necessary for compatibility.  Hopefully in a few years if I need to update again I can remove that legacy code.

 

Solution

   Below is the script to enumerate SharePoint 2010 and 2013 permissions down to site level.  You can also download it from my SkyDrive account or my posting on the TechNet Script Center Repository.

SkyDrive

TechNet Script Center Repository

http://gallery.technet.microsoft.com/scriptcenter/Enumerate-SharePoint-2010-35976bdb

########################################################### 
#DisplaySPWebApp8.ps1 
# 
#Author: Brian T. Jackett 
#Last Modified Date: 2013-07-01 
# 
#Traverse the entire web app site by site to display 
# hierarchy and users with permissions to site. 
########################################################### 

function Expand-ADGroupMembership 
{ 
    Param 
    ( 
        [Parameter(Mandatory=$true, 
                   Position=0)] 
        [string] 
        $ADGroupName, 
        [Parameter(Position=1)] 
        [string] 
        $RoleBinding 
    ) 
    Process 
    { 
        $roleBindingText = “” 
        if(-not [string]::IsNullOrEmpty($RoleBinding)) 
        { 
            $roleBindingText = ” RoleBindings=`”$roleBindings`”” 
        } 
        Write-Output “<ADGroup Name=`”$($ADGroupName)`”$roleBindingText>” 
        $domain = $ADGroupName.substring(0, $ADGroupName.IndexOf(“”) + 1) 
        $groupName = $ADGroupName.Remove(0, $ADGroupName.IndexOf(“”) + 1) 
                            
        #BEGIN – CODE ADAPTED FROM SCRIPT CENTER SAMPLE CODE REPOSITORY 
        #http://www.microsoft.com/technet/scriptcenter/scripts/powershell/search/users/srch106.mspx 
        #GET AD GROUP FROM DIRECTORY SERVICES SEARCH 
        $strFilter = “(&(objectCategory=Group)(name=”+($groupName)+“))” 
        $objDomain = New-Object System.DirectoryServices.DirectoryEntry 
        $objSearcher = New-Object System.DirectoryServices.DirectorySearcher 
        $objSearcher.SearchRoot = $objDomain 
        $objSearcher.Filter = $strFilter 
        # specify properties to be returned 
        $colProplist = (“name”,“member”,“objectclass”) 
        foreach ($i in $colPropList) 
        { 
            $catcher = $objSearcher.PropertiesToLoad.Add($i) 
        } 
        $colResults = $objSearcher.FindAll() 
        #END – CODE ADAPTED FROM SCRIPT CENTER SAMPLE CODE REPOSITORY 
        foreach ($objResult in $colResults) 
        { 
            if($objResult.Properties[“Member”] -ne $null) 
            { 
                foreach ($member in $objResult.Properties[“Member”]) 
                { 
                    $indMember = [adsi] “LDAP://$member” 
                    $fullMemberName = $domain + ($indMember.Name) 
                    
                    #if($indMember[“objectclass”] 
                        # if child AD group continue down chain 
                        if(($indMember | Select-Object -ExpandProperty objectclass) -contains “group”) 
                        { 
                            Expand-ADGroupMembership -ADGroupName $fullMemberName 
                        } 
                        elseif(($indMember | Select-Object -ExpandProperty objectclass) -contains “user”) 
                        { 
                            Write-Output “<ADUser>$fullMemberName</ADUser>” 
                        } 
                } 
            } 
        } 
        
        Write-Output “</ADGroup>” 
    } 
} #end Expand-ADGroupMembership 
# main portion of script 
if((Get-PSSnapin -Name microsoft.sharepoint.powershell) -eq $null) 
{ 
    Add-PSSnapin Microsoft.SharePoint.PowerShell 
} 
$farm = Get-SPFarm 
Write-Output “<Farm Guid=`”$($farm.Id)`”>” 
$webApps = Get-SPWebApplication 
foreach($webApp in $webApps) 
{ 
    Write-Output “<WebApplication URL=`”$($webApp.URL)`” Name=`”$($webApp.Name)`”>” 
    foreach($site in $webApp.Sites) 
    { 
        Write-Output “<SiteCollection URL=`”$($site.URL)`”>” 
        
        foreach($web in $site.AllWebs) 
        { 
            Write-Output “<Site URL=`”$($web.URL)`”>” 
            # if site inherits permissions from parent then stop processing 
            if($web.HasUniqueRoleAssignments -eq $false) 
            { 
                Write-Output “<!– Inherits role assignments from parent –>” 
            } 
            # else site has unique permissions 
            else 
            { 
                foreach($assignment in $web.RoleAssignments) 
                { 
                    if(-not [string]::IsNullOrEmpty($assignment.Member.Xml)) 
                    { 
                        $roleBindings = ($assignment.RoleDefinitionBindings | Select-Object -ExpandProperty name) -join “,” 
                        # check if assignment is SharePoint Group 
                        if($assignment.Member.XML.StartsWith(‘<Group’) -eq “True”) 
                        { 
                            Write-Output “<SPGroup Name=`”$($assignment.Member.Name)`” RoleBindings=`”$roleBindings`”>” 
                            foreach($SPGroupMember in $assignment.Member.Users) 
                            { 
                                # if SharePoint group member is an AD Group 
                                if($SPGroupMember.IsDomainGroup) 
                                { 
                                    Expand-ADGroupMembership -ADGroupName $SPGroupMember.Name 
                                } 
                                # else SharePoint group member is an AD User 
                                else 
                                { 
                                    # remove claim portion of user login 
                                    #Write-Output “<ADUser>$($SPGroupMember.UserLogin.Remove(0,$SPGroupMember.UserLogin.IndexOf(“|”) + 1))</ADUser>” 
                                    Write-Output “<ADUser>$($SPGroupMember.UserLogin)</ADUser>” 
                                } 
                            } 
                            Write-Output “</SPGroup>” 
                        } 
                        # else an indivdually listed AD group or user 
                        else 
                        { 
                            if($assignment.Member.IsDomainGroup) 
                            { 
                                Expand-ADGroupMembership -ADGroupName $assignment.Member.Name -RoleBinding $roleBindings 
                            } 
                            else 
                            { 
                                # remove claim portion of user login 
                                #Write-Output “<ADUser>$($assignment.Member.UserLogin.Remove(0,$assignment.Member.UserLogin.IndexOf(“|”) + 1))</ADUser>” 
                                
                                Write-Output “<ADUser RoleBindings=`”$roleBindings`”>$($assignment.Member.UserLogin)</ADUser>” 
                            } 
                        } 
                    } 
                } 
            } 
            Write-Output “</Site>” 
            $web.Dispose() 
        } 
        Write-Output “</SiteCollection>” 
        $site.Dispose() 
    } 
    Write-Output “</WebApplication>” 
} 
Write-Output “</Farm>” 

   The output from the script can be sent to an XML which you can then explore using the [XML] type accelerator.  This lets you explore the XML structure however you see fit.  See the screenshot below for an example.

SP20102013Permissions1

 

   If you do view the XML output through a text editor (Notepad++ for me) notice the format.  Below we see a SharePoint site that has a SharePoint group Demo Members with Edit permissions assigned.  Demo Members has an AD group corpdevelopers as a member.  corpdevelopers has a child AD group called corpDevelopersSub with 1 AD user in that sub group.  As you can see the script recursively expands the AD hierarchy.

SP20102013Permissions2

 

Conclusion

   It took me 4 years to finally update this script but I‘m happy to get this published.  I was able to fix a number of errors and smooth out some rough edges.  I plan to develop this into a more full fledged tool over the next year with more features and flexibility (copy permissions, search for individual user or group, optional enumerate lists / items, etc.).  If you have any feedback, feature requests, or issues running it please let me know.  Enjoy the script!

 

      -Frog Out

The Power of PowerShell and SharePoint: Enumerating SharePoint Permissions and Active Directory

<Update 2013-07-01> This script has been updated for SharePoint 2010 / 2013.  Please see my updated script and blog post at PowerShell
Script to Enumerate SharePoint 2010 or 2013 Permissions and Active Directory
Group Membership
.

</Update 2013-07-01>

<Update>

Posting code didn’t format as well as hoped.  Download the below script here.

</Update>

For those of you who are SharePoint admins or developers but have never dug into the SharePoint API or PowerShell, I would recommend first checking out some tutorials on both and referencing the SharePoint Developer Center.  At a later date I hope to be able to provide some quick demo scripts that highlight the power, time savings, and overall usefulness that can be gained by combining PowerShell and the SharePoint API.  For now though I wish to post a script I developed almost a year ago as a side project to combine a number of powerful features into one script.  To start, let me overview what the below script is capable of.

  1. Recursively crawl a site or entire web application within SharePoint
  2. Enumerate permissions assigned to a SharePoint site
  3. Detail the SharePoint users assigned to a SharePoint group
  4. Determine if an Active Directory group is a member of a SharePoint group
  5. Detail the Active Directory users who are members of an Active Directory group
  6. Search for a specific user’s permissions on a SharePoint site

Before anyone says anything, yes I realize that combining so many utilities into one script is probably a bad design and I should’ve broken out functionality.  Yes this is probably true, but I want to state that this script was never intended for Production release.  Instead I was prototyping what was possible with PowerShell and I even surprised myself with what I ended up with.  Here is an attempt to visualize what the above hierarchy would look like.

–Site

——SharePoint User A

——SharePoint Group A

————SharePoint User B

————Active Directory Group A

——————Active Directory User A

——————Active Directory User B

As you can see, this allows you to dig much further than what you might normally surface from the SharePoint API.  The true purpose of this script was to determine if a user was assigned permissions anywhere within a web application, even if indirectly by membership in a SharePoint group or Active Directory group.  This was only ever intended for a test environment, so you may still find some bugs when running against your own environment.

Before running this, ensure that you have loaded the SharePoint assembly with the following call (typically placed into your PowerShell profile for ease of use):

[void][System.Reflection.Assembly]::LoadWithPartialName(“Microsoft.SharePoint”)

Please leave me feedback if you end up trying out this script or have any questions on how/why I wrote things the way I did.  I always enjoy constructive criticism and dialog.  If you do re-post this anywhere, be sure to include the reference to the source material for the Active Directory call portion as I borrowed it from the PowerShell Script Center.

Example call:

.DisplaySPWebApp6.ps1 http://server WebApp userA

 

Note: The below script does not format properly through WordPress after I migrated my blog.  Please refer to the source script for better view.


###########################################################
#DisplaySPWebApp6.ps1 -URL  -searchScope  -userToFind 
#
#Author: Brian Jackett
#Last Modified Date: Jan. 12, 2009
#
#Supply Traverse the entire web app site by site to display
# hierarchy and users with permissions to site.
###########################################################

&nbsp;

#DECLARE VARIABLES
[string]$siteUrl = $args[0]
[string]$searchScope = $args[1]
[string]$userToFind = $args[2]

#DECLARE CONSTANTS
$BUFFER_CHARS = " "

function DetermineSpaceBuffer #-iterations 
{
[string]$spaceBuffer = ""
for($i = 0; $i -lt $args[0]; $i++)
{$spaceBuffer += $BUFFER_CHARS}

return $spaceBuffer
}

#DECLARE FUNCTIONS
function DrillDownADGroup #-group  -depth 
{
[string]$spaceBuffer = DetermineSpaceBuffer $args[1]
$domain = $args[0].Name.substring(0, $args[0].Name.IndexOf("\") + 1)
$groupName = $args[0].Name.Remove(0, $args[0].Name.IndexOf("\") + 1)

#BEGIN - CODE ADAPTED FROM SCRIPT CENTER SAMPLE CODE REPOSITORY
#http://www.microsoft.com/technet/scriptcenter/scripts/powershell/search/users/srch106.mspx

#GET AD GROUP FROM DIRECTORY SERVICES SEARCH
$strFilter = "(&amp;(objectCategory=Group)(name="+($groupName)+"))"
$objDomain = New-Object System.DirectoryServices.DirectoryEntry
$objSearcher = New-Object System.DirectoryServices.DirectorySearcher
$objSearcher.SearchRoot = $objDomain
$objSearcher.Filter = $strFilter

#
$colProplist = ("name","member")
foreach ($i in $colPropList)
{
$catcher = $objSearcher.PropertiesToLoad.Add($i)
}
$colResults = $objSearcher.FindAll()

#END - CODE ADAPTED FROM SCRIPT CENTER SAMPLE CODE REPOSITORY

&nbsp;

foreach ($objResult in $colResults)
{
foreach ($member in $objResult.Properties.member)
{
$indMember = [adsi] "LDAP://$member"

#ATTEMPT TO GET AD OBJECT TYPE FOR USER, NOT WORKING RIGHT NOW
#$user = $indMember.PSBase
#$user.Properties

$fullUserName = $domain + ($indMember.Name)
DisplayADEntry $fullUserName ($args[1])
}
}
}

function DisplaySPGroupMembers #-group  -depth 
{
[string]$spaceBuffer = DetermineSpaceBuffer $args[1]

if($args[0].Users -ne $Null)
{
#START SHAREPOINT USERS ENTITY
Write-Output $spaceBuffer""

foreach($user in $args[0].Users)
{
DisplayADEntry $user ($args[1] + 1)
}

#END SHAREPOINT USERS ENTITY
Write-Output $spaceBuffer""
}
}

function DisplayADEntry #-user/group  -depth 
{
#FILTER RESULTS IF LOOKING FOR SPECIFIC USER
if($args[0].IsDomainGroup -eq "True")
{
$outputText = "$spaceBuffer$BUFFER_CHARS" + ($args[0])
Write-Output $outputText
DrillDownADGroup $args[0] ($args[1])
$outputText = "$spaceBuffer$BUFFER_CHARS"
Write-Output $outputText
}
else
{
#USER FOUND AS A CHILD OF AN EMBEDDED AD GROUP
if(($userToFind -ne "" -and ($userToFind.ToUpper() -eq $args[0].LoginName.ToUpper() -or $userToFind.ToUpper() -eq $args[0].ToUpper())) -or $userToFind -eq "")
{
$outputText = "$spaceBuffer$BUFFER_CHARS" + ($args[0]) + ""
Write-Output $outputText
}
}
}

function DetermineUserAccess #-web  -depth 
{
[string]$spaceBuffer = DetermineSpaceBuffer $args[1]

#START SHAREPOINT GROUPS ENTITY
Write-Output "$spaceBuffer"

foreach($perm in $args[0].Permissions)
{
#CHECK IF MEMBER IS AN ACTIVE DIRECTORY ENTRY OR SHAREPOINT GROUP
if($perm.XML.Contains('MemberIsUser="True"') -eq "True")
{
DisplayADEntry $perm.Member ($args[1] + 1)
}
#IS A SHAREPOINT GROUP
else
{
$outputText = "$spaceBuffer$BUFFER_CHARS" + ($perm.Member)
Write-Output $outputText
DisplaySPGroupMembers $perm.Member ($args[1] + 2)
Write-Output "$spaceBuffer$BUFFER_CHARS"
}
}

#END SHAREPOINT GROUPS ENTITY
Write-Output "$spaceBuffer"
}

function DisplayWebApplication #-webApp 
{
[string]$spaceBuffer = DetermineSpaceBuffer $args[1]

#START WEB APPLICATION ENTITY
$outputText = "$spaceBuffer" + ($args[0].Name)
Write-Output $outputText

if($args[0].Sites -ne $Null)
{
#START CONTAINED SITE COLLECTIONS ENTITY
Write-Output "$spaceBuffer$BUFFER_CHARS"

foreach($spSiteColl in $args[0].Sites)
{
DisplaySiteCollection $spSiteColl ($args[1] + 2)
$spSiteColl.Dispose()
}

#END CONTAINED SITE COLLECTIONS ENTITY
Write-Output "$spaceBuffer$BUFFER_CHARS"
}

#END WEB APPLICATION ENTITY
"$spaceBuffer"
}

function DisplaySiteCollection #-siteColl  -depth 
{
[string]$spaceBuffer = DetermineSpaceBuffer $args[1]
$sc = $args[0].OpenWeb()

#START SITE COLLECTION ENTITY
$outputText = "$spaceBuffer" + ($sc.URL)
Write-Output $outputText

if($sc -ne $Null)
{
#START CONTAINED SITES ENTITY
Write-Output "$spaceBuffer$BUFFER_CHARS"

foreach ($spWeb in $sc)
{
DisplayWeb $spWeb ($args[1] + 2)
$spWeb.Dispose()
}

#END CONTAINED SITES ENTITY
Write-Output "$spaceBuffer$BUFFER_CHARS"
}

#END SITE COLLECTION ENTITY
Write-Output "$spaceBuffer"

#CLEANUP SITE COLLECTION VARIABLE
$sc.Dispose()
}

function DisplayWeb #-web  -depth  -parentWeb 
{
[string]$spaceBuffer = DetermineSpaceBuffer $args[1]

#START SITE ENTITY
$outputText = "$spaceBuffer" + ($args[0].URL)
Write-Output $outputText

if($args[0].HasUniquePerm -eq "True")
{
DetermineUserAccess $args[0] ($args[1] + 1)
}
else
{
Write-Output "$spaceBuffer<!--Inherits from parent&gt;-->"
}

&nbsp;

if($args[0].Webs -ne $Null)
{
#START CONTAINED SUBSITES ENTITY
Write-Output "$spaceBuffer$BUFFER_CHARS"

#RECURSIVELY SEARCH SUBWEBS
foreach ($spSubWeb in $args[0].Webs)
{
DisplayWeb $spSubWeb ($args[1] + 2)
$spSubWeb.Dispose()
}
#END CONTAINED SUBSITES ENTITY
Write-Output "$spaceBuffer$BUFFER_CHARS"
}

#END SITE ENTITY
Write-Output "$spaceBuffer"
}

function DisplayMissingParametersMessage
{
#Write-Output "You are missing a parameter for 'Site URL'"
$script:siteURL = Read-Host "Enter Site URL"
}

############
# MAIN
############

#IF MISSING PARM FOR SITE URL, ASK FOR INPUT TO FILL
if($args.length -eq 0)
{
DisplayMissingParametersMessage
}

$rootSite = New-Object Microsoft.SharePoint.SPSite($siteUrl)
$spWebApp = $rootSite.WebApplication

&nbsp;

Write-Output ""

#IF SEARCH SCOPE SPECIFIED FOR SITE, ONLY SEARCH SITE
if($searchScope -eq "-site")
{
DisplaySiteCollection $rootSite 1
}
#ELSE SEARCH ENTIRE WEB APP
else
{
DisplayWebApplication $spWebApp 1
}
Write-Output ""

&nbsp;

#CLEANUP
$rootSite.Dispose()