Category Archives: Automation

Azure AD app registration and scope + roles modification (manifest) as application

As the ‘new’ Graph API does not support application credentials (client_credentials oauth2 flow) when working with most of the ServicePrincipal and Application parts of the Graph API, and I really did not want to work with a user account (background processes, MFA, etc), I had to work something out on some of the older (but still supported) API’s which I gleaned from Msft’s PS modules.

For anyone googling, the following code example allows you to create an azure ad application with serviceprincipal and allows you to modify the manifest of the application (e.g. here is the AppRoles and AppIdentifier).

$tenantId = "75d24247-6221-46a1-a651-530ae36dd399"
$clientId = "62d2235b-2ef6-4d70-b273-401c9eb450b3" #client ID (to call graph api with)
$clientSecret = "xxxxxx" #client secret

[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Web")
$body = @{client_id=$clientId;client_secret=$clientSecret;resource='https://graph.windows.net/';grant_type='client_credentials'}
$headers = @{"Authorization" = "Bearer $((invoke-webrequest -uri "https://login.microsoftonline.com/$tenantId/oauth2/token" `
-Method POST -ContentType "application/x-www-form-urlencoded" -Body $body).Content | convertfrom-json | select access_token -ExpandProperty access_token)"
}

#finding an application or create it if it doesn't exist
$appName = "MyApplication"
$app = (Invoke-RestMethod -Method GET -UseBasicParsing -Uri "https://graph.windows.net/$tenantId/applications?api-version=1.6&`$filter=displayName eq %27$appName%27" -Headers $headers -ContentType "application/json").value
if(!$app){
    $body = [pscustomobject]@{
	    'displayName' = $appName
        'availableToOtherTenants' = $True
    }
    $app = Invoke-RestMethod -Method POST -UseBasicParsing -Uri "https://graph.windows.net/$tenantId/applications?api-version=1.6" -Headers $headers -Body ($body | ConvertTo-Json -Depth 100) -ContentType "application/json"
}

#check/correct the identifier URI of the application's published API
if($app[0].identifierUris -notcontains "api://mydomain/myApi"){
    $body = [PSCustomObject]@{
        "identifierUris" = @("api://mydomain/myApi")
    }
    Invoke-RestMethod -Method PATCH -UseBasicParsing -Uri "https://graph.windows.net/$tenantId/applications/$($app[0].objectId)?api-version=1.6" -ContentType "application/json" -Headers $headers -Body ($body | ConvertTo-Json -Depth 100)
}

#check if a certain approle is present, if not, add it
if(@($app[0].appRoles | Where{$_.displayName -eq "Access To My API"}).Count -eq 0){
    $body = [PSCustomObject]@{
        "appRoles" = @([PSCustomObject]@{
            "allowedMemberTypes" = @("Application","User")
            "description" = "Access To My API"
            "displayName" = "Access To My API"
            "id" = [Guid]::NewGuid()
            "isEnabled" = $True
            "value" = "Api.AccessRead"
            }
        )
    }
    Invoke-RestMethod -Method PATCH -UseBasicParsing -Uri "https://graph.windows.net/$tenantId/applications/$($app[0].objectId)?api-version=1.6" -ContentType "application/json" -Headers $headers -Body ($body | ConvertTo-Json -Depth 100)
}

#finding the serviceprincipal belonging to the application or creating it if it doesn't exist
$sp = (Invoke-RestMethod -Method GET -UseBasicParsing -Uri "https://graph.windows.net/$tenantId/servicePrincipals?api-version=1.6&`$filter=displayName eq %27$appName%27" -Headers $headers -ContentType "application/json").value
if(!$sp){
    ##Adding service principal to application instance
    $body = [pscustomobject]@{
	    'appId' = $app.appId
        'tags' = @("WindowsAzureActiveDirectoryIntegratedApp")
    }
    $sp = Invoke-RestMethod -Method POST -UseBasicParsing -Uri "https://graph.windows.net/$tenantId/servicePrincipals/?api-version=1.6" -Headers $headers -Body ($body | ConvertTo-Json -Depth 100) -ContentType "application/json"
}

Note that, for this code to work, you need to grant your application the Company Administrator role, like this:

	Connect-AzureAD
	$app = Get-AzureADServicePrincipal -SearchString "myapplication"
	$role = Get-AzureADDirectoryRole | Where-Object { $_.DisplayName -eq "Company Administrator" }
Add-AzureADDirectoryRoleMember -ObjectId $role.ObjectId -RefObjectId $app.ObjectId

Using runbooks interactively in PowerApps (Build your own app in < 10 minutes!)

Mostly, the users of my PowerShell scripts are themselves PowerShell users. Sometimes though, the audience is less tech-savvy. In this blog post (with my first EVER video tutorial!) I’ll show you how to give your users a super user friendly interface to your scripts: Microsoft PowerApps.

You’ll need a PowerApps trial or license to follow this tutorial.

When you use the Azure AD group that was created to publish your app to when it is ready for distribution, your users will automatically be granted the correct permissions in Azure to start a runbook, as PowerApps does not use its own identity when interacting with connectors, it impersonates the user identity.

The source code for the runbook is:

Param(
    [String]$searchParameter
)

$uri = "https://techcommunity.microsoft.com/t5/forums/searchpage/tab/message?advanced=false&amp;allow_punctuation=false&amp;q=$searchParameter"

Start-Sleep -s 2

Write-Output "Runbook started, searching for $searchParameter..."

$res = Invoke-WebRequest -Uri $uri -UseBasicParsing -Method GET -ErrorAction Stop

Start-Sleep -s 2

Write-Output "found some results, analyzing...."

$firstHit = $res.Links | where-object {$_.outerHTML -like "*lia-link-navigation*" -and $_.href -like "/t5/*"} | select href -First 1 -ExpandProperty href
$firstHit = "https://techcommunity.microsoft.com/$firstHit"

Start-Sleep -s 2

Write-Output "Retrieving first 100 characters of first result..."

$res = Invoke-WebRequest -Uri $firstHit -UseBasicParsing -Method GET -ErrorAction Stop
$excerpt = $res.Content.Substring(($res.Content.IndexOf("class=`"lia-message-body-content`"")+64),100) -Replace('<[^>]+>','')

Start-Sleep -s 2

Write-Output "Result:"
Write-Output $excerpt
write-Output ""
write-Output ""
write-Output ""
write-Output "source: $firstHit"

The app screen’s OnStart property’s function is:

Set(runbookOutput,Blank());Set(runbookJobId,Blank());Set(runbookActive,false);Set(runbookResult,Blank())

The search button’s function is:

Set(runbookResult,Blank());Set(runbookOutput,Blank());Set(runbookJobId,Blank());Set(runbookActive,true);Set(runbookJobId,'new-searchQuery'.Run(TextInput2.Text).jobid)

The status label’s function is:

If(IsBlank(runbookResult) && runbookActive = false," ",If(runbookActive,"Please wait for job to complete…",Concatenate("Job result: ",runbookResult)))

The timer OnTimerStart function is:

If(runbookActive && Len(runbookJobId) > 5,Set(runbookOutput,'get-searchQueryOutput'.Run(runbookJobId).joboutput))

The timer OnTimerEnd function is:

If(runbookActive && Len(runbookJobId) > 5,Set(runbookResult,'get-searchQueryStatus'.Run(runbookJobId).jobstatus));If(runbookResult = "Completed" Or runbookResult = "Suspended" Or runbookResult = "Stopped",Set(runbookActive,false));

Exporting ALL file info in O365 (Sharepoint, Teams, Onedrive, Groups)

For a recent customer case, we needed a meta-data export of ALL files in the entire O365 tenant, filenames, dates, modifications, sizes and unique ID’s.

It is tricky to actually get everything (Teams especially) and to handle any MFA you may (SHOULD!!) have configured on your admin accounts, but here you are:

https://gitlab.com/Lieben/assortedFunctions/blob/master/export-allO365FilesToCsv.ps1

Note that your admin account does have to have permissions on all sources the script identifies.

Max Path Length in Sharepoint Online, Onedrive for Business and Teams

Many Office 365 clients (and especially Excel) really don’t like long paths. The older the client, the worse the issues.

To find files over a certain path length in Office 365, I wrote a script a while back. This required manual perusal of the results and manual correction of the issues.

The new version of my script now features fully automatic discovery, a flexible editor and automatic correction of paths based on what you entered into the editor.

Correcting path lengths in Sharepoint Online, Onedrive for Business and Microsoft Teams

You can get the PowerShell script / editor from my git repository here.

Many thanks to CTS in their help me designing and testing this script!

How to use

  1. download the script and save it somewhere
  2. Copy the desired Sharepoint Library into a new Sharepoint Library and/or site
  3. Run the script only for that site or library by specifying the -specificSiteUrls or specificDocumentLibraries parameter
  4. do some corrections, commit them, check the results
  5. If satisfied, set permissions for your admin account on the actual production locations your wish to fix (script for Onedrive Mass Permissions here)
  6. Ensure no users have open files in the library
  7. Remove any non-standard characters from folder names (see script source, example code at the top)
  8. Run

Disclaimer

  • Please make sure you TEST this on a copy of your document library/sites before using the ‘commit’ option.
  • Use at your own risk.
  • I recommend turning on the Universal Audit log before usage.

Azure update management error

For those googling this error in the Update Management console in Azure:

System.Runtime.InteropServices.COMException (0x80240438): Exception from HRESULT: 0x80240438    at Microsoft.EnterpriseManagement.Mom.Modules.ChangeTracking.WUA.IUpdateSearcher2.EndSearch(ISearchJob searchJob)    at Microsoft.EnterpriseManagement.Advisor.PatchManagement.WindowsUpdateHelper.GetUpdateSnapshot(TimeSpan timeout, Boolean onlineSearch, DateTime lastTimeUpdateApplied, IAutomaticUpdates2 automaticUpdates, UpdateModuleState state)

Fix: Exempt the server from group policies (or alternative solutions) that configure Windows Update. GPO’s override Azure Update Management and block the Azure agent from searching for updates.