Skip to content

Instantly share code, notes, and snippets.

@KentNordstrom
Created April 15, 2018 14:43
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save KentNordstrom/88e47e3665943ba12e0fa0c2eb5f31aa to your computer and use it in GitHub Desktop.
Save KentNordstrom/88e47e3665943ba12e0fa0c2eb5f31aa to your computer and use it in GitHub Desktop.
Use the Microsoft Generic PowerShell Connector to connect to MS Graph
param (
[System.Collections.ObjectModel.KeyedCollection[[string], [Microsoft.MetadirectoryServices.ConfigParameter]]] $ConfigParameters,
[PSCredential] $PSCredential,
[Microsoft.MetadirectoryServices.OpenImportConnectionRunStep] $OpenImportConnectionRunStep,
[Microsoft.MetadirectoryServices.Schema] [ValidateNotNull()] $Schema
)
[string]$watermark = $OpenImportConnectionRunStep.CustomData #Used if Delta Import
if($OpenImportConnectionRunStep.ImportType -eq 'Delta' -and !$watermark){throw ("Full Import Required. No watermark found.")}
Write-EventLog -LogName Application -Source "FIMSynchronizationService" -EventId 6801 -Category 1 -Message ("Starting " + $OpenImportConnectionRunStep.ImportType +" import using watermark: " + $watermark) -EntryType Information
(New-Object Microsoft.MetadirectoryServices.OpenImportConnectionResults($watermark)) #Return
<#
CommonModule.psm1 for Microsoft Generic PowerShell Connector used in FIM/MIM Synchronization Service
#>
Set-StrictMode -Version "2.0"
function New-FIMSchema {
[CmdletBinding()]
[OutputType([Microsoft.MetadirectoryServices.Schema])]
param()
[Microsoft.MetadirectoryServices.Schema]::Create()
}
function New-FIMSchemaType
{
[CmdletBinding()]
[OutputType([Microsoft.MetadirectoryServices.SchemaType])]
param
(
[ValidateNotNullOrEmpty()]
[string] $Name,
[switch] $LockAnchorAttributeDefinition
)
[Microsoft.MetadirectoryServices.SchemaType]::Create($Name, $LockAnchorAttributeDefinition.ToBool())
}
function Add-FIMSchemaAttribute
{
[CmdletBinding(DefaultParameterSetName = 'SingleValued')]
[OutputType([Microsoft.MetadirectoryServices.SchemaAttribute])]
param
(
[Parameter(Mandatory, ValueFromPipeline)]
[ValidateNotNull()]
[Microsoft.MetadirectoryServices.SchemaType] $InputObject,
[Parameter(Mandatory, ParameterSetName='Anchor')]
[Parameter(Mandatory, ParameterSetName = 'MultiValued')]
[Parameter(Mandatory, ParameterSetName = 'SingleValued')]
[ValidateNotNullOrEmpty()]
[string] $Name,
[Parameter(ParameterSetName='Anchor')]
[switch] $Anchor,
[Parameter(ParameterSetName = 'MultiValued')]
[switch] $Multivalued,
[Parameter(Mandatory, ParameterSetName='Anchor')]
[Parameter(Mandatory, ParameterSetName = 'MultiValued')]
[Parameter(Mandatory, ParameterSetName = 'SingleValued')]
[ValidateSet('Binary', 'Boolean', 'Integer', 'Reference', 'String')]
[string] $DataType,
[Parameter(Mandatory, ParameterSetName='Anchor')]
[Parameter(Mandatory, ParameterSetName = 'MultiValued')]
[Parameter(Mandatory, ParameterSetName = 'SingleValued')]
[ValidateSet('ImportOnly', 'ExportOnly', 'ImportExport')]
[string] $SupportedOperation
)
switch ($PSCmdlet.ParameterSetName) {
'SingleValued' {
$InputObject.Attributes.Add([Microsoft.MetadirectoryServices.SchemaAttribute]::CreateSingleValuedAttribute($Name, $DataType, $SupportedOperation))
}
'MultiValued' {
if ($Multivalued) {
$InputObject.Attributes.Add([Microsoft.MetadirectoryServices.SchemaAttribute]::CreateMultiValuedAttribute($Name, $DataType, $SupportedOperation))
} else {
$InputObject.Attributes.Add([Microsoft.MetadirectoryServices.SchemaAttribute]::CreateSingleValuedAttribute($Name, $DataType, $SupportedOperation))
}
}
'Anchor' {
if ($Anchor) {
$InputObject.Attributes.Add([Microsoft.MetadirectoryServices.SchemaAttribute]::CreateAnchorAttribute($Name, $DataType, $SupportedOperation))
} else {
$InputObject.Attributes.Add([Microsoft.MetadirectoryServices.SchemaAttribute]::CreateSingleValuedAttribute($Name, $DataType, $SupportedOperation))
}
}
}
}
function New-GenericObject
{
param (
[string] $typeName = $(throw "Please specify a generic type name")
,[string[]] $typeParameters = $(throw "Please specify the type parameters")
,[object[]] $constructorParameters
)
$genericTypeName = $typeName + '`' + $typeParameters.Count
$genericType = [Type]$genericTypeName
if (!$genericType)
{
throw "Could not find generic type $genericTypeName"
}
## Bind the type arguments to it
$typedParameters = [type[]] $typeParameters
$closedType = $genericType.MakeGenericType($typedParameters)
if (!$closedType)
{
throw "Could not make closed type $genericType"
}
## Create the closed version of the generic type, don't forget comma prefix
,[Activator]::CreateInstance($closedType, $constructorParameters)
}
function Get-SchemaFromXml
{
param (
[string] $xmlFilePath = $(throw "Please specify the xml file path to read schema from")
)
$x = [xml](Get-Content $xmlFilePath)
$schema = [Microsoft.MetadirectoryServices.Schema]::Create()
foreach ($t in $x.Schema.Types.SchemaType)
{
$lockAnchorDefinition = $true
if ($t.LockAnchorDefinition -eq "0")
{
$lockAnchorDefinition = $false
}
$schemaType = [Microsoft.MetadirectoryServices.SchemaType]::Create($t.Name, $lockAnchorDefinition)
if ($t.GetElementsByTagName("PossibleDNComponentsForProvisioning").Count -gt 0)
{
foreach ($c in $t.PossibleDNComponentsForProvisioning)
{
$schemaType.PossibleDNComponentsForProvisioning.Add($c)
}
}
foreach ($a in $t.Attributes.SchemaAttribute)
{
if ($a.IsAnchor -eq 1)
{
$schemaType.Attributes.Add([Microsoft.MetadirectoryServices.SchemaAttribute]::CreateAnchorAttribute($a.Name, $a.DataType, $a.AllowedAttributeOperation))
}
elseif ($a.IsMultiValued -eq 1)
{
$schemaType.Attributes.Add([Microsoft.MetadirectoryServices.SchemaAttribute]::CreateMultiValuedAttribute($a.Name, $a.DataType, $a.AllowedAttributeOperation))
}
else
{
$schemaType.Attributes.Add([Microsoft.MetadirectoryServices.SchemaAttribute]::CreateSingleValuedAttribute($a.Name, $a.DataType, $a.AllowedAttributeOperation))
}
}
$schema.Types.Add($schemaType)
}
return $schema
}
function Get-CSEntryChangeValue
{
param (
[Microsoft.MetadirectoryServices.CSEntryChange] $csentryChange = $(throw "Please specify csentryChange parameter.")
,[string] $attributeName = $(throw "Please specify attributeName parameter.")
,[object] $defaultValue = $null
,[switch] $oldValue
)
if ($csentryChange.AttributeChanges.Contains($attributeName))
{
$returndefault = $true
$attributeChange = $csentryChange.AttributeChanges[$attributeName]
foreach ($valueChange in $attributeChange.ValueChanges)
{
if ($oldValue)
{
if ($valueChange.ModificationType -eq "Delete")
{
$valueChange.Value # return
$returndefault = $false
}
}
else
{
if ($valueChange.ModificationType -eq "Add")
{
$valueChange.Value # return
$returndefault = $false
}
}
}
if ($returndefault)
{
$defaultValue # return
}
}
else
{
$defaultValue # return
}
}
<#
Parameter $category can have one of these numerical values:
1 = Management Agent Run Profile
2 = Database
3 = Server
4 = Web Server
5 = Security
#>
function Write-ErrorToEventLog
{
param ([string] $errorMsg, [int] $category)
Write-EventLog -LogName Application -Source "FIMSynchronizationService" -EventId 6801 -Category $category -Message $errorMsg -EntryType Error
}
function Write-InfoToEventLog
{
param ([string] $infoMsg, [int] $category)
Write-EventLog -LogName Application -Source "FIMSynchronizationService" -EventId 6801 -Category $category -Message $infoMsg -EntryType Information
}
function Get-MSGraphAuthenticationToken {
[CmdletBinding()]
param(
[parameter(Mandatory=$true, HelpMessage="A tenant name should be provided in the following format: tenantname.onmicrosoft.com.")]
[ValidateNotNullOrEmpty()]
[string]$TenantName,
[parameter(Mandatory=$true, HelpMessage="Application ID for an Azure AD application.")]
[ValidateNotNullOrEmpty()]
[string]$ClientID,
[parameter(Mandatory=$true, HelpMessage="Application Secret for an Azure AD application.")]
[ValidateNotNullOrEmpty()]
[string]$ClientSecret,
[parameter(Mandatory=$false, HelpMessage="Redirect URI for Azure AD application. Leave empty to leverage Azure PowerShell well known redirect URI.")]
[string]$RedirectUri,
[parameter(Mandatory=$false, HelpMessage="AD Module installed. AzureAD or AzureADPreview. Default is AzureAD.")]
[string]$ADModule="AzureAD"
)
# Load assemblies
try {
# Get installed Azure AD modules
$AzureADModules = Get-InstalledModule -Name $ADModule
if ($AzureADModules -ne $null) {
# Check if multiple modules exist and determine the module path for the most current version
if (($AzureADModules | Measure-Object).Count -gt 1) {
$LatestAzureADModule = ($AzureADModules | Select-Object -Property Version | Sort-Object)[-1]
$AzureADModulePath = $AzureADModules | Where-Object { $_.Version -like $LatestAzureADModule.Version } | Select-Object -ExpandProperty InstalledLocation
}
else {
$AzureADModulePath = Get-InstalledModule -Name $ADModule | Select-Object -ExpandProperty InstalledLocation
}
# Construct array for required assemblies from Azure AD module
$Assemblies = @(
(Join-Path -Path $AzureADModulePath -ChildPath "Microsoft.IdentityModel.Clients.ActiveDirectory.dll"),
(Join-Path -Path $AzureADModulePath -ChildPath "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll")
)
Add-Type -Path $Assemblies -ErrorAction Stop
try {
$Authority = "https://login.microsoftonline.com/$($TenantName)/oauth2/token"
$ResourceRecipient = "https://graph.microsoft.com"
$Credentials = New-Object -TypeName "Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential" -ArgumentList $ClientID,$ClientSecret
$AuthenticationContext = New-Object -TypeName "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $Authority
$PlatformParams = New-Object -TypeName "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Always" # Arguments: Auto, Always, Never, RefreshSession
$AuthenticationResult = ($AuthenticationContext.AcquireTokenAsync($ResourceRecipient, $Credentials)).Result
# Check if access token was acquired
if ($AuthenticationResult.AccessToken -ne $null) {
# Construct authentication hash table for holding access token and header information
$Authentication = @{
"Content-Type" = "application/json"
"Authorization" = -join("Bearer ", $AuthenticationResult.AccessToken)
}
# Return the authentication token
return $Authentication
}
else {
Write-Warning -Message "Failure to acquire access token. Response with access token was null" ; break
}
}
catch [System.Exception] {
Write-Warning -Message "An error occurred when constructing an authentication token: $($_.Exception.Message)" ; break
}
}
else {
Write-Warning -Message "Azure AD PowerShell module is not present on this system, please install before you continue" ; break
}
}
catch [System.Exception] {
Write-Warning -Message "Unable to load required assemblies (Azure AD PowerShell module) to construct an authentication token. Error: $($_.Exception.Message)" ; break
}
}
<#
.SYNOPSIS
Example script to create a registered application that for example can be used when connecting using PowerShell to the MS Graph.
To be able to manage the identity used and verify data I use the AzureADPreview Module and AzureRM Module.
#>
#We use the AzureRM module to store the AppKey in the KeyVault. SKip this if already installed.
Install-Module AzureADPreview -Scope AllUsers
Install-Module AzureRM -Scope AllUsers
#Import relevant Modules
Import-Module AzureRM
Import-Module AzureADPreview
#We have a few parameters we need to set
$SubscriptionName = "MyIAMSubscription"
$VaultName = "IAMVault"
$appDisplayName = "MIM GraphConnector"
$appIdentifierUris = "https://GraphConnector.konab.net"
$appHomePage = "https://GraphConnector.konab.net"
$appReplyUrls = @($appIdentifierUris)
#Add accounts to be used to configure Azure resources
$AzureRMAdmin = Login-AzureRmAccount -SubscriptionName $SubscriptionName
$AzureADAdmin = Connect-AzureAD
#Get the MS Graph
$MSGraph = Get-AzureADServicePrincipal | ?{$_.DisplayName -match "Microsoft Graph"}
<#
Key now is to figure out the Permissions in the Microsoft Graph you would like to assign to the Connector.
Example of permissions in the MS Graph
Read All Directory Data: $MSGraph.AppRoles | ?{$_.Value -match "Directory.Read.All"}
Read AuditLogs: $MSGraph.AppRoles | ?{$_.Value -match "AuditLog.Read.All"}
Read Users: $MSGraph.AppRoles | ?{$_.Value -match "User.Read.All"}
Read and Write Groups: $MSGraph.AppRoles | ?{$_.Value -match "Group.ReadWrite.All"}
Best practice would be to create a new Role in Azure with the Permissions we need and assign the app to that role. https://docs.microsoft.com/en-us/azure/role-based-access-control/custom-roles
Below in the example I assign the Directory.Read.All permission.
#>
$reqAccess = New-Object -TypeName "Microsoft.Open.AzureAD.Model.RequiredResourceAccess"
$reqAccess.ResourceAppId = $MSGraph.AppId
$access = New-Object -TypeName "Microsoft.Open.AzureAD.Model.ResourceAccess" -ArgumentList ($MSGraph.AppRoles | ?{$_.Value -match "Directory.Read.All"}).Id,"Role"
$reqAccess.ResourceAccess = $access
#We can now register the application with correct permissions and with some password to use
#First we generate the client secret for the application
$PasswordCredential = New-Object -TypeName Microsoft.Open.AzureAD.Model.PasswordCredential
#AppKey is a random 36 character string in this example
$AppKey = -join ((65..72) + (74..78) +(80..90) + (97..107) + (50..57) + (109..110) + (112..122)| Get-Random -Count 36 | % {[char]$_})
$AppKeyStartDate = Get-Date
$AppKeyEndDate = $AppKeyStartDate.AddYears(3) #3 Years before I have to change the Secret
$PasswordCredential.StartDate = $AppKeyStartDate
$PasswordCredential.EndDate = $AppKeyEndDate
$PasswordCredential.KeyId = New-Guid
$PasswordCredential.Value = $AppKey
#We should store the AppKey in the KeyVault to make sure we have it stored in a secure way until we need it.
$SecretName = "MIMGraphConnector"
$Secret = ConvertTo-SecureString -String $AppKey -AsPlainText -Force
$KeyVaultSecret = Set-AzureKeyVaultSecret -VaultName $VaultName -Name $SecretName -SecretValue $Secret
#Finally time to actually create the app in Azure AD and add it as ServicePrincipal
$app = New-AzureADApplication -DisplayName $appDisplayName -IdentifierUris $appIdentifierUris -Homepage $appHomePage -ReplyUrls $appReplyUrls -PasswordCredentials $PasswordCredential -RequiredResourceAccess $reqAccess
$sp = New-AzureADServicePrincipal -AppId $app.AppId
#NOTE!
#The $app.AppId value is the Client Id value we use in the connector
#The $PasswordCredential.Value ($AppKey) is the Client Secret value we use in the connector
#Before the new ServicePrincipal can be used you need to go into the Azure Portal and Grant Permission (Perform Admin Consent)
param (
[System.Collections.ObjectModel.KeyedCollection[[string], [Microsoft.MetadirectoryServices.ConfigParameter]]] $ConfigParameters,
[PSCredential] $PSCredential,
[Microsoft.MetadirectoryServices.OpenImportConnectionRunStep] $OpenImportConnectionRunStep,
[Microsoft.MetadirectoryServices.CloseImportConnectionRunStep] $CloseImportConnectionRunStep
)
[string]$watermark = $CloseImportConnectionRunStep.CustomData
Write-EventLog -LogName Application -Source "FIMSynchronizationService" -EventId 6801 -Category 1 -Message ("Ending " + $OpenImportConnectionRunStep.ImportType +" import saving new watermark: " + $watermark) -EntryType Information
(New-Object Microsoft.MetadirectoryServices.CloseImportConnectionResults($watermark)) #Return
param (
[System.Collections.ObjectModel.KeyedCollection[[string], [Microsoft.MetadirectoryServices.ConfigParameter]]] $ConfigParameters,
[PSCredential] $PSCredential,
[Microsoft.MetadirectoryServices.ImportRunStep] $GetImportEntriesRunStep,
[Microsoft.MetadirectoryServices.OpenImportConnectionRunStep] $OpenImportConnectionRunStep,
[Microsoft.MetadirectoryServices.Schema] [ValidateNotNull()] $Schema
)
#Import the common module
Import-Module (Join-Path -Path ([Microsoft.MetadirectoryServices.MAUtils]::MAFolder) -ChildPath "CommonModule.psm1")
[string]$watermark = $GetImportEntriesRunStep.CustomData #Used in Delta Import
$importType = $OpenImportConnectionRunStep.ImportType
#region ConfigParameters
[bool]$debug = $ConfigParameters["Debug_Global"].Value
$AzureADModule = if($ConfigParameters["AzureADModule_Global"].Value -match "AzureAD"){$ConfigParameters["AzureADModule_Global"].Value}else{"AzureAD"}
$TenantName = $ConfigParameters["Domain"].Value
$ClientId = $PSCredential.UserName.Split('\')[1] #Domain comes as part of PSCredential
$ClientSecret = $PSCredential.GetNetworkCredential().password
#endregion ConfigParameters
if($debug){Write-InfoToEventLog -infoMsg ("Starting Import using: TenantName="+$TenantName+", ClientId="+$ClientId) -category 1}
$importEntriesResults = New-Object -TypeName 'Microsoft.MetadirectoryServices.GetImportEntriesResults'
$importEntriesResults.CSEntries = New-Object -TypeName 'System.Collections.Generic.List[Microsoft.MetadirectoryServices.CSEntryChange]'
#Get AuthHeader
try{
$AuthHeader = Get-MSGraphAuthenticationToken -TenantName $TenantName -ClientID $ClientId -ClientSecret $ClientSecret -ADModule $AzureADModule
}
catch{
Write-ErrorToEventLog -infoMsg ("Error getting AuthHeader") -category 1
throw ("Error getting authentication header.")
}
#region Import Users
$AttributesToGet = $Schema.Types["user"].Attributes.Name -join ','
if($importType -eq 'Delta')
{
$Uri = $watermark
}
else
{
$Uri = "https://graph.microsoft.com/beta/users/delta?select="+$AttributesToGet
}
if($debug){Write-InfoToEventLog -infoMsg ("Getting users using Uri: " + $Uri) -category 1}
do{
$response = (Invoke-RestMethod -Method Get -Uri $Uri -Headers $AuthHeader)
$Uri = $response.'@odata.nextlink'
$users = $users + $response.value
}until ($Uri -eq $null)
#endregion Import Users
#region Add CSEntries
foreach($user in $users)
{
$csentry = [Microsoft.MetadirectoryServices.CSEntryChange]::Create()
$csentry.ObjectModificationType = "Add"
$csentry.ObjectType="user"
foreach ($attribute in $Schema.Types["user"].Attributes)
{
$value = $user.($attribute.Name)
if($value)
{
if($attribute.Name -eq "provisionedPlans")
{
#provisionedPlans are returned as Objects and are converted to MultiValue string objects converted to JSON.
[string[]]$mv = $value | ForEach-Object {$_ | ConvertTo-Json -Compress} | Sort-Object | Get-Unique
[void] $csentry.AttributeChanges.Add([Microsoft.MetadirectoryServices.AttributeChange]::CreateAttributeAdd($attribute.Name, $mv))
}
else
{
[void] $csentry.AttributeChanges.Add([Microsoft.MetadirectoryServices.AttributeChange]::CreateAttributeAdd($attribute.Name, $value))
}
}
}
$importEntriesResults.CSEntries.Add($csentry)
}
#endregion Add CSEntries
$importEntriesResults.MoreToImport = $false
$importEntriesResults.CustomData = $response.'@odata.deltaLink'
$importEntriesResults #Return
Import-Module (Join-Path -Path ([Environment]::GetEnvironmentVariable("TEMP", [EnvironmentVariableTarget]::Machine)) -ChildPath "CommonModule.psm1") -Verbose:$false
$Schema = New-FIMSchema
$SchemaType = New-FIMSchemaType -Name "user" -LockAnchorAttributeDefinition
$SchemaType | Add-FIMSchemaAttribute -Name "id" -Anchor -DataType "String" -SupportedOperation ImportExport
$SchemaType | Add-FIMSchemaAttribute -Name "userPrincipalName" -DataType "String" -SupportedOperation ImportExport
$SchemaType | Add-FIMSchemaAttribute -Name "provisionedPlans" -DataType "String" -MultiValued -SupportedOperation ImportExport
$SchemaType | Add-FIMSchemaAttribute -Name "userType" -DataType "String" -SupportedOperation ImportExport
$SchemaType | Add-FIMSchemaAttribute -Name "accountEnabled" -DataType "Boolean" -SupportedOperation ImportExport
$Schema.Types.Add($SchemaType)
$Schema #Return
@KentNordstrom
Copy link
Author

I have noticed that the Graph "loop" somtimes gives duplicates in large environments. If you run into that issue you can sort and get unique objects by adding the following to Import.ps1 before creating CSEntries.
$users = $users | Sort-Object -Property id -Unique

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment