From 779e72d878f96d64b429144d7b3fd0fae53f27c7 Mon Sep 17 00:00:00 2001 From: craig-martin Date: Tue, 14 May 2024 00:10:44 +0000 Subject: [PATCH] adds functions for generating DSC configuration from MIM Service objects --- ...nvert-MimSvcObjectToDscDependsOnString.ps1 | 29 +++ .../Convert-MimSvcObjectToDscItemName.ps1 | 36 +++ Functions/Convert-MimSvcObjectToDscScript.ps1 | 211 ++++++++++++++++++ ...nvert-MimSvcReferenceToDscLookupString.ps1 | 97 ++++++++ .../Convert-MimSvcReferenceToMimObject.ps1 | 117 ++++++++++ MimDsc.psd1 | 5 + 6 files changed, 495 insertions(+) create mode 100644 Functions/Convert-MimSvcObjectToDscDependsOnString.ps1 create mode 100644 Functions/Convert-MimSvcObjectToDscItemName.ps1 create mode 100644 Functions/Convert-MimSvcObjectToDscScript.ps1 create mode 100644 Functions/Convert-MimSvcReferenceToDscLookupString.ps1 create mode 100644 Functions/Convert-MimSvcReferenceToMimObject.ps1 diff --git a/Functions/Convert-MimSvcObjectToDscDependsOnString.ps1 b/Functions/Convert-MimSvcObjectToDscDependsOnString.ps1 new file mode 100644 index 0000000..a116fa5 --- /dev/null +++ b/Functions/Convert-MimSvcObjectToDscDependsOnString.ps1 @@ -0,0 +1,29 @@ +function Convert-MimSvcObjectToDscDependsOnString +{ + param + ( + <# + A MIM Service Object to convert + #> + [parameter(Mandatory=$true, ValueFromPipeline = $true)] + $MimObject + ) + begin + { + $dependsOnStrings = @() + } + process + { + $dscConfigurationItemName = Convert-MimSvcObjectToDscItemName -MimObject $MimObject + Write-Verbose " DependsOn [$($MimObject.ObjectType)]$($dscConfigurationItemName)" + $dependsOnStrings += ("'[{0}]{1}'" -F ($MimObject.ObjectType -replace '-'), $dscConfigurationItemName) + } + end + { + ### Only output if we have items + if ($dependsOnStrings.Count -gt 0) + { + Write-Output ($dependsOnStrings -join ',') + } + } +} \ No newline at end of file diff --git a/Functions/Convert-MimSvcObjectToDscItemName.ps1 b/Functions/Convert-MimSvcObjectToDscItemName.ps1 new file mode 100644 index 0000000..069c2c0 --- /dev/null +++ b/Functions/Convert-MimSvcObjectToDscItemName.ps1 @@ -0,0 +1,36 @@ +function Convert-MimSvcObjectToDscItemName +{ + param + ( + <# + A MIM Service Object to convert + #> + [parameter(Mandatory=$true, ValueFromPipeline = $true)] + $MimObject + ) + + ### + ### Get the attribute that is unique for this object type + if ($MimObject.ObjectType -in 'ObjectTypeDescription','AttributeTypeDescription') + { + $mimKeyAttributeValue = $MimObject.Name + } + elseif($MimObject.ObjectType -eq 'BindingDescription') + { + ### Custom Configuration AttributeName for BindingDescription + $BoundAttribute = Get-Resource -ID $MimObject.BoundAttributeType + $BoundObject = Get-Resource -ID $MimObject.BoundObjectType + + $mimKeyAttributeValue = "{0}{1}" -F $BoundObject.Name, $BoundAttribute.Name + } + elseif($MimObject.ObjectType -in 'HomePageConfiguration','SearchScopeConfiguration') + { + $mimKeyAttributeValue = "{0}{1}" -F $MimObject.DisplayName, $MimObject.Order + } + else + { + $mimKeyAttributeValue = $MimObject.DisplayName + } + ## Do some sanitizing for the DSC item name + Write-Output ($mimKeyAttributeValue -replace ' ' -replace '\)' -replace '\(' -replace '-' -replace "'" -replace ':' -replace "’" -replace '&' -replace '/' -replace ',') +} \ No newline at end of file diff --git a/Functions/Convert-MimSvcObjectToDscScript.ps1 b/Functions/Convert-MimSvcObjectToDscScript.ps1 new file mode 100644 index 0000000..4bb7dc0 --- /dev/null +++ b/Functions/Convert-MimSvcObjectToDscScript.ps1 @@ -0,0 +1,211 @@ +function Convert-MimSvcObjectToDscScript +{ + param + ( + [parameter(Mandatory=$true, ValueFromPipeline = $true)] + $MimObject, + [Switch]$SkipGuidConversion, + [Switch]$SkipDependsOnForSchema + ) + begin + { + $includedAttributes = @('Description','Filter','AccountName','DisplayName','FirstName','LastName','DisplayedOwner','MembershipAddWorkflow','MembershipLocked','Owner','Scope','Type','Domain') + $mimReferenceValues = @() + } + process + { + Write-Verbose "[$(Get-Date)] MIM Object: [$($MimObject.ObjectType)]$($MimObject.ObjectID)" + $mimObjectType = $MimObject.ObjectType + + $supportedObjectTypes = @( + 'ActivityInformationConfiguration' + 'AttributeTypeDescription' + 'BindingDescription' + 'DomainConfiguration' + 'EmailTemplate' + 'FilterScope' + 'ForestConfiguration' + 'Group' + 'HomepageConfiguration' + #'ma-data' + 'ManagementPolicyRule' + 'msidmSystemConfiguration' + 'NavigationBarConfiguration' + 'ObjectTypeDescription' + 'ObjectVisualizationConfiguration' + 'Person' + 'PortalUIConfiguration' + 'Resource' + 'SearchScopeConfiguration' + 'Set' + 'SynchronizationFilter' + 'SynchronizationRule' + 'SystemResourceRetentionConfiguration' + 'TimeZoneConfiguration' + 'WorkflowDefinition' + ) + if ($MimObject.ObjectType -notin $supportedObjectTypes) + { + Write-Warning " Object type not currently supported: $($MimObject.ObjectType)" + return + } + + ### + ### Check to see if we've already processed this item + ### + if ($mimObject.ObjectID -in $global:processedObjectIDs) + { + Write-Warning (" This objectID has already been processed already: {0}" -F $mimObject.ObjectID) + return + } + else + { + Write-Debug (" Adding ObjectID to the list of items already processed: {0}" -F $mimObject.ObjectID) + $global:processedObjectIDs += $mimObject.ObjectID + } + + ### Get the DSC type and DSC item name for this object + $dscResourceType = "$($MimObject.ObjectType)" -replace '-' + $dscItemName = Convert-MimSvcObjectToDscItemName -MimObject $MimObject + $mimDscString = "$dscResourceType $dscItemName`n{`n" + + ### Find the longest attribute name to determine the column width + $columnWidth = $mimObject | Get-Member -MemberType NoteProperty | Select -ExpandProperty Name | Select-Object -ExpandProperty Length | Sort-Object| Select-Object -Last 1 + Write-Debug "Using a column width of $columnWidth" + + <# + ### Get the DSC resource for this object type + if ($Script:DscResourceProperties.ContainsKey($dscResourceType)) + { + $dscResourcePropertyNames = $Script:DscResourceProperties[$dscResourceType] + } + else + { + $dscResource = Get-DscResource -Name $dscResourceType + $dscResourcePropertyNames = $dscResource.Properties | Select-Object -ExpandProperty Name + + $Script:DscResourceProperties.Add($dscResourceType, $dscResourcePropertyNames) + } + #> + + ##HACK - rename 'Scope' to 'GroupScope' + if (($mimObject | Get-Member -MemberType NoteProperty).Name -contains 'Scope') + { + #$scopeAttribute = $mimObject | Get-Member -MemberType NoteProperty | Where AttributeName -eq 'Scope' + #$scopeAttribute.AttributeName = 'GroupScope' + Write-Warning "Hacked out hack - this could break something" + } + + $mimDependsOnValues = @() + + foreach($mimAttribute in ($mimObject | Get-Member -MemberType NoteProperty)) + { + Write-Debug " Processing attribute: $($mimAttribute.AttributeName)" + if ($mimAttribute.Name -in 'AuthNWFLockedOut','AuthNWFRegistered','AuthNLockoutRegistrationID','DomainConfiguration','ObjectID','CreatedTime','Creator','ResourceTime','DeletedTime','ObjectType','DetectedRulesList','ExpectedRulesList','ExpirationTime','MVObjectID') + { + Write-Debug " Skipping system-owned attribute: $($mimAttribute.Name)" + continue + } + + if ($mimAttribute.Name -in 'ExplicitMember','ComputedMember') + { + Write-Debug " Skipping user-owned attribute: $($mimAttribute.Name)" + continue + } + + <# + if ($mimAttribute.Name -notin $dscResourcePropertyNames) + { + Write-Warning " Skipping attribute because there is no matching property in the DSC resource: $($mimAttribute.Name)" + } + #> + $attributeType = Search-Resources -XPath "/AttributeTypeDescription[Name = '$($mimAttribute.Name)']" -AttributesToGet Name, DataType,Multivalued + + $mimDscAttributeString = '' + if ($attributeType.MultiValued -and $attributeType.DataType -eq 'Reference') + { + $mimAttributeValues = @() + foreach($mimAttributeValue in $mimObject.($mimAttribute.Name)) + { + + $mimAttributeValues += Convert-MimSvcReferenceToDscLookupString -ReferenceString $mimAttributeValue + $mimDependsOnValues += Convert-MimSvcReferenceToMimObject -ReferenceString $mimAttributeValue -SkipDependsOnForSchema:$SkipDependsOnForSchema | Convert-MimSvcObjectToDscDependsOnString + $mimReferenceValues += $mimAttributeValue + } + $mimDscAttributeString = $mimAttributeValues -join ',' + } + elseif ($attributeType.MultiValued) + { + $mimAttributeValues = @() + foreach($mimAttributeValue in $mimObject.($mimAttribute.Name)) + { + $mimAttributeValues += "'$mimAttributeValue'" + } + $mimDscAttributeString = $mimAttributeValues -join ',' + } + else + { + if ($attributeType.DataType -eq 'Reference') + { + if ([String]::IsNullOrEmpty($mimObject.($mimAttribute.Name))) + { + Write-Debug " Skipping attribute because there is no attribute value: $($mimAttribute.Name)" + } + else + { + $mimDscAttributeString = Convert-MimSvcReferenceToDscLookupString -ReferenceString $mimObject.($mimAttribute.Name) + $mimDependsOnValues += Convert-MimSvcReferenceToMimObject -ReferenceString $mimObject.($mimAttribute.Name) -SkipDependsOnForSchema:$SkipDependsOnForSchema | Convert-MimSvcObjectToDscDependsOnString + $mimReferenceValues += $mimObject.($mimAttribute.Name) + } + } + else + { + $tryParseResult = $false + if ([boolean]::TryParse($mimObject.($mimAttribute.Name),[ref]$tryParseResult)) + { + $mimDscAttributeString = '${0}' -F $tryParseResult + } + else + { + $mimDscAttributeString = "'$($mimObject.($mimAttribute.Name) -replace "'","''" -replace "’","''")'" + } + } + } + + ### Only output the property if there is a property value + if ([String]::IsNullOrEmpty($mimObject.($mimAttribute.Name))) + { + Write-Debug " Skipping attribute because there is no attribute value: $($mimAttribute.Name)" + } + else + { + $mimDscString += "`t{0,-$columnWidth} = {1}`n" -F $mimAttribute.Name, $mimDscAttributeString + } + } + + if ($mimDependsOnValues.Count -gt 0) + { + $mimDscString += "`t{0,-$columnWidth} = {1}`n" -F 'DependsOn', (($mimDependsOnValues | Select-Object -Unique) -join ',') + } + $mimDscString += "`t{0,-$columnWidth} = '{1}'`n" -F 'Ensure', 'Present' + $mimDscString += "}`n" + + Write-Output ([PSCustomObject]@{ + ObjectType = $mimObjectType + ObjectIdentifier = $mimObject.ObjectID + DscItemScript = $mimDscString + }) + } + end + { + $exportsToProcess = $mimReferenceValues | Convert-MimSvcReferenceToMimObject -SkipDependsOnForSchema:$SkipDependsOnForSchema | Sort-Object -Property ObjectID -Unique + foreach($exportToProcess in $exportsToProcess) + { + if ($exportToProcess.ObjectID -notin $global:processedObjectIDs) + { + #Write-Host " DependsOn [$($exportToProcess.ObjectType)] $($exportToProcess.ObjectID)" + Convert-MimSvcObjectToDscScript -MimObject $exportToProcess | Write-Output + } + } + } +} \ No newline at end of file diff --git a/Functions/Convert-MimSvcReferenceToDscLookupString.ps1 b/Functions/Convert-MimSvcReferenceToDscLookupString.ps1 new file mode 100644 index 0000000..78dbfc2 --- /dev/null +++ b/Functions/Convert-MimSvcReferenceToDscLookupString.ps1 @@ -0,0 +1,97 @@ +function Convert-MimSvcReferenceToDscLookupString +{ + param + ( + <# + A String the MIM ObjectID(s) to convert + This is typically a single ID such as 'urn:uuid:1ef3d501-3c7b-42ad-8407-2c51cbb7a09b' + or a more complex string containing multiple IDs, such as a XOML or Filter + #> + [parameter(Mandatory=$true, ValueFromPipeline = $true)] + $ReferenceString + ) + process + { + ### + ### Value will either be a GUID or a STRING containing multiple GUIDS + ### + if ($ReferenceString -match "^urn:uuid:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}") + { + ### + ### Get the MIM object + ### + $mimObject = Get-Resource -ID $ReferenceString -AttributesToGet ObjectType, Name, TimeZoneId, AccountName, DisplayName + + ### + ### Return if the referenced object is not found + ### + if (-not $mimObject) + { + Write-Warning "Could not find MIM object for this ObjectID: $ReferenceString" + return + } + + ### + ### Get the attribute that is unique for this object type + ### + if ($mimObject.ObjectType -in 'ObjectTypeDescription','AttributeTypeDescription') + { + Write-Output "'$($mimObject.Name -replace "'", "''" -replace "’", "''" -replace "’", "''")'" + } + elseif($mimObject.ObjectType -eq 'TimeZoneConfiguration') + { + Write-Output "'$($mimObject.TimeZoneId -replace "'", "''" -replace "’", "''" -replace "’", "''")'" + } + elseif($mimObject.ObjectType -eq 'Person') + { + Write-Output "'$($mimObject.AccountName -replace "'", "''" -replace "’", "''" -replace "’", "''")'" + } + else + { + Write-Output "'$($mimObject.DisplayName -replace "'", "''" -replace "’", "''" -replace "’", "''")'" + } + } + else + { + $regex = [regex]"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" + if ($regex.IsMatch($ReferenceString)) + { + foreach($regexMatch in $regex.Matches($ReferenceString)) + { + $mimObject = Get-Resource -ID $regexMatch.Value -AttributesToGet ObjectType, Name, TimeZoneId, AccountName, DisplayName + if ($mimObject) + { + ### + ### Get the attribute that is unique for this object type + ### + ### TODO - do we really need the string replaces here? + if ($mimObject.ObjectType -in 'ObjectTypeDescription','AttributeTypeDescription') + { + $mimAttributeName = 'Name' + $mimAttributeValue = $mimObject.Name + } + elseif($mimObject.ObjectType -eq 'Person') + { + $mimAttributeName = 'AccountName' + $mimAttributeValue = $mimObject.AccountName + } + else + { + $mimAttributeName = 'DisplayName' + $mimAttributeValue = $mimObject.DisplayName + } + $jsonString = '{{ObjectType:"{0}",AttributeName:"{1}",AttributeValue:"{2}"}}' -F $mimObject.ObjectType, $mimAttributeName, $mimAttributeValue + $ReferenceString = $ReferenceString -replace $regexMatch.Value, $jsonString + } + } + } + + ## Only output if this string was created + if ($ReferenceString) + { + $ReferenceString = $ReferenceString -replace "'","''" -replace "’","''" -replace "´","''" + Write-Output "'$ReferenceString'" + } + } + } +} \ No newline at end of file diff --git a/Functions/Convert-MimSvcReferenceToMimObject.ps1 b/Functions/Convert-MimSvcReferenceToMimObject.ps1 new file mode 100644 index 0000000..8eac867 --- /dev/null +++ b/Functions/Convert-MimSvcReferenceToMimObject.ps1 @@ -0,0 +1,117 @@ +function Convert-MimSvcReferenceToMimObject +{ + param + ( + <# + A String the MIM ObjectID(s) to convert + This is typically a single ID such as 'urn:uuid:1ef3d501-3c7b-42ad-8407-2c51cbb7a09b' + or a more complex string containing multiple IDs, such as a XOML or Filter + #> + [parameter(Mandatory=$true, ValueFromPipeline = $true)] + $ReferenceString, + [Switch]$SkipDependsOnForSchema + ) + process + { + ### + ### Value will either be a GUID or a STRING containing multiple GUIDS + ### + if ($ReferenceString -match "^urn:uuid:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}") + { + ### + ### Get the MIM object + ### + if ($global:mimConfigHashTable) + { + $mimObject = $global:fimConfigHashTable[$ReferenceString] + } + else + { + $mimObjectType = Get-Resource -ID ($ReferenceString -replace 'urn:uuid:') -AttributesToGet ObjectType | Select -ExpandProperty ObjectType + + $attributesToGet = Get-MimSvcSchemaCache -ObjectType $mimObjectType | Select -Expand Name | Where {$_ -notin @('ComputedMember','ExplicitMember','DetectedRulesList','ExpectedRulesList','MVObjectID','ExpirationTime')} | Where {$_ -notlike 'syncconfig-*'} + + $mimObject = Get-Resource -ID ($ReferenceString -replace 'urn:uuid:') -AttributesToGet $attributesToGet + } + + ### + ### Return if the referenced object is not found + ### + if (-not $mimObject) + { + Write-Warning "Could not find MIM object for this ObjectID: $ReferenceString" + return + } + + ### + ### Skip if Person or Group object type to avoid (StrongConnect : DependsOn link exceeded max depth limitation '1024') + ### + if ($mimObject.ObjectType -in 'Person','Group','Computer','ma-data','ForestConfiguration') + { + return + } + + ### + ### Skip if Schema + ### + if ($SkipDependsOnForSchema -and $mimObject.ObjectType -in 'ObjectTypeDescription','AttributeTypeDescription','BindingDescription') + { + return + } + + $mimObject | Write-Output + } + else + { + $dependsOnStrings = @() + $regex = [regex]"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" + foreach($regexMatch in $regex.Matches($ReferenceString)) + { + if ($regexMatch.Value -eq [Guid]::Empty) + { + continue + } + + if ($global:mimConfigHashTable) + { + $refObject = $global:mimConfigHashTable["urn:uuid:$($regexMatch.Value)"] + } + else + { + $refObjectType = Get-Resource -ID $regexMatch.Value -AttributesToGet ObjectType | Select -ExpandProperty ObjectType + + $attributesToGet = Get-MimSvcSchemaCache -ObjectType $refObjectType | Select -Expand Name | Where {$_ -notin @('ComputedMember','ExplicitMember','DetectedRulesList','ExpectedRulesList','MVObjectID','ExpirationTime')} | Where {$_ -notlike 'syncconfig-*'} + + $refObject = Get-Resource -ID $regexMatch.Value -AttributesToGet $attributesToGet + } + + ### + ### Skip this loop iteration if the referenced object is not found + ### + if (-not $refObject) + { + Write-Warning "Could not find MIM object for this ObjectID: $($regexMatch.Value)" + continue + } + + ### + ### Skip if Person or Group object type to avoid (StrongConnect : DependsOn link exceeded max depth limitation '1024') + ### + if ($refObject.ObjectType -in 'Person','Group','Computer') + { + continue + } + + ### + ### Skip if Schema + ### + if ($SkipDependsOnForSchema -and $refObject.ObjectType -in 'ObjectTypeDescription','AttributeTypeDescription','BindingDescription') + { + return + } + + $refObject | Write-Output + } + } + } +} \ No newline at end of file diff --git a/MimDsc.psd1 b/MimDsc.psd1 index b24078f..bbe3088 100644 --- a/MimDsc.psd1 +++ b/MimDsc.psd1 @@ -66,6 +66,11 @@ ScriptsToProcess = @( # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. FunctionsToExport = @( 'Convert-MimSvcExportToPSObject' + 'Convert-MimSvcObjectToDscDependsOnString' + 'Convert-MimSvcObjectToDscItemName' + 'Convert-MimSvcObjectToDscScript' + 'Convert-MimSvcReferenceToDscLookupString' + 'Convert-MimSvcReferenceToMimObject' 'Convert-MimSyncConfigToDsc' 'Convert-MimSyncJoinCriterionToCimInstance' 'Convert-MimSyncMVObjectTypeToCimInstance'