Skip to content

Commit

Permalink
RegistryPolicyFile: Gpt.ini Creation/Modification Fix (#19)
Browse files Browse the repository at this point in the history
* Added Set-GptIniFile logic to address issue #18

* moved signature data to .Common directory

* created incrementing function

* updated functions with comment based help

* updated tests

* updated code to address pester test failure.

* updated comment.

* updated the case on Param to param based on
failed test

* addressed code review comments

* updated to use pathintrinsics for path management
  • Loading branch information
bcwilhite authored Mar 13, 2020
1 parent a02ee86 commit 3f0c234
Show file tree
Hide file tree
Showing 6 changed files with 462 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- GPRegistryPolicyDsc
- Update GitVersion.yml with the correct regular expression.
- Added GPT.ini creation/updating logic in order to properly apply Group Policy.

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ function Get-TargetResource
.PARAMETER TargetType
Indicates the target type. This is needed to determine the .pol file path. Supported values are LocalMachine, User, Administrators, NonAdministrators, Account.
.PARAMETER AccountName
Specifies the name of the account for an user specific pol file to be managed.
Expand Down Expand Up @@ -199,6 +199,16 @@ function Set-TargetResource
}
}

# write the gpt.ini update
$setGptIniFileParams = @{
TargetType = $TargetType
}
if ($PSBoundParameters.ContainsKey('AccountName'))
{
$setGptIniFileParams.AccountName = $AccountName
}

Set-GptIniFile @setGptIniFileParams
Set-RefreshRegistryKey
}

Expand All @@ -220,7 +230,7 @@ function Set-TargetResource
.PARAMETER TargetType
Indicates the target type. This is needed to determine the .pol file path. Supported values are LocalMachine, User, Administrators, NonAdministrators, Account.
.PARAMETER AccountName
Specifies the name of the account for an user specific pol file to be managed.
Expand Down Expand Up @@ -318,7 +328,7 @@ function Get-RegistryPolicyFilePath
param
(
[Parameter(Mandatory = $true)]
[ValidateSet("ComputerConfiguration","UserConfiguration","Administrators","NonAdministrators","Account")]
[ValidateSet('ComputerConfiguration','UserConfiguration','Administrators','NonAdministrators','Account')]
[System.String]
$TargetType,

Expand Down Expand Up @@ -387,7 +397,7 @@ function ConvertTo-SecurityIdentifier
<#
.SYNOPSIS
Converts a SID to an NTAccount name.
.PARAMETER SecurityIdentifier
Specifies SID of the identity to convert.
#>
Expand Down Expand Up @@ -441,3 +451,271 @@ function Set-RefreshRegistryKey
New-Item -Path $Path -Force
New-ItemProperty -Path $Path -Name $PropertyName -Value $Value -Force
}

<#
.SYNOPSIS
Sets the gpt.ini file according to user/computer policy changes.
.PARAMETER TargetType
Indicates the target type. This is needed to determine the gpt.ini file path. Supported values are LocalMachine, User, Administrators, NonAdministrators, Account.
.PARAMETER AccountName
Specifies the name of the account for an user specific gpt.ini file to be managed.
#>
function Set-GptIniFile
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[ValidateSet('ComputerConfiguration','UserConfiguration','Administrators','NonAdministrators','Account')]
[System.String]
$TargetType,

[Parameter()]
[System.String]
$AccountName
)

$registryPolicyPath = Split-Path -Path (Get-RegistryPolicyFilePath @PSBoundParameters) -Parent | Split-Path -Parent
$gptIniPath = Join-Path -Path $registryPolicyPath -ChildPath 'gpt.ini'

$extensionNamesPattern = '{35378EAC-683F-11D2-A89A-00C04FBBCFA2}.*{D02B1F7[2|3]-3407-48AE-BA88-E8213C6761F1}'
$extensionHashtable = @{
gPCMachineExtensionNames = '35378EAC-683F-11D2-A89A-00C04FBBCFA2', 'D02B1F72-3407-48AE-BA88-E8213C6761F1'
gPCUserExtensionNames = '35378EAC-683F-11D2-A89A-00C04FBBCFA2', 'D02B1F73-3407-48AE-BA88-E8213C6761F1'
}

# Detect gPCMachineExtensionNames/gPCUserExtensionNames presence and value
foreach ($gPCItem in $extensionHashtable.Keys)
{
$gptEntry = Get-PrivateProfileString -AppName 'General' -KeyName $gPCItem -GptIniPath $gptIniPath
if (-not ($gptEntry -match $extensionNamesPattern))
{
if ($gptEntry -ne [String]::Empty)
{
$gPCExistingValue = $gptEntry -replace '\[{|}]' -split '}{'
$gPCNewValue = $gPCExistingValue + $extensionHashtable[$gPCItem] | Select-Object -Unique | Sort-Object
}
else
{
$gPCNewValue = $extensionHashtable[$gPCItem]
}

$formattedgPCNewValue = '[{{{0}}}]' -f $($gPCNewValue -join '}{')
Write-Verbose -Message ($script:localizedData.GptIniCseUpdate -f $gPCItem, $gptEntry, $formattedgPCNewValue)
Write-PrivateProfileString -AppName 'General' -KeyName $gPCItem -KeyValue $formattedgPCNewValue -GptIniPath $gptIniPath
}

<#
To ensure consistent gpt.ini file structure, querying Version and setting Version so the structure will be:
gPC[User|Machine]ExtensionName = [{guids}]
Version = 11111
gPC[User|Machine]ExtensionName = [{guids}]
#>
$gptVersion = Get-PrivateProfileString -AppName 'General' -KeyName 'Version' -Default 0 -GptIniPath $gptIniPath
Write-PrivateProfileString -AppName 'General' -KeyName 'Version' -KeyValue $gptVersion -GptIniPath $gptIniPath
}

# Determine incremented version number
$newGptVersion = Get-IncrementedGptVersion -TargetType $TargetType -Version $gptVersion

# Write incremented version to GPT
Write-Verbose -Message ($script:localizedData.GptIniVersionUpdate -f $TargetType, $gptVersion, $newGptVersion)
Write-PrivateProfileString -AppName 'General' -KeyName 'Version' -KeyValue $newGptVersion -GptIniPath $gptIniPath
}

<#
.SYNOPSIS
Queries an ini file for specific information.
.PARAMETER AppName
The name of the section containing the key name in an ini file, also known as 'Section'.
.PARAMETER KeyName
The name of the key whose associated string is to be retrieved.
.PARAMETER Default
If the KeyName key cannot be found in the initialization file, GetPrivateProfileString
copies the default string to the ReturnedString buffer.
.PARAMETER GptIniPath
Path to the gpt.ini file to be queried.
#>
function Get-PrivateProfileString
{
[CmdletBinding()]
[OutputType([System.String])]
param
(
[Parameter()]
[System.String]
$AppName = 'General',

[Parameter(Mandatory = $true)]
[System.String]
$KeyName,

[Parameter()]
[System.String]
$Default,

[Parameter(Mandatory = $true)]
[System.String]
$GptIniPath
)

# The GetPrivateProfileString method requires a FileSystem path, meaning no PSDrive paths
$fullyQualifiedFilePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($GptIniPath)

$stringBuilder = [System.Text.StringBuilder]::new(65535)

[void][GPRegistryPolicyDsc.IniUtility]::GetPrivateProfileString(
$AppName,
$KeyName,
$Default,
$stringBuilder,
$stringBuilder.Capacity,
$fullyQualifiedFilePath
)

return $stringBuilder.ToString()
}

<#
.SYNOPSIS
Writes information to an ini file.
.PARAMETER AppName
The name of the section containing the key name in an ini file, also known as 'Section'.
.PARAMETER KeyName
The name of the key whose associated KeyValue string is to be written/modified.
.PARAMETER KeyValue
A null-terminated string to be written to the file.
.PARAMETER GptIniPath
Path to the gpt.ini file to be written/modified.
#>
function Write-PrivateProfileString
{
[CmdletBinding()]
[OutputType([System.String])]
param
(
[Parameter()]
[System.String]
$AppName = 'General',

[Parameter(Mandatory = $true)]
[System.String]
$KeyName,

[Parameter(Mandatory = $true)]
[System.String]
$KeyValue,

[Parameter(Mandatory = $true)]
[System.String]
$GptIniPath
)

# The WritePrivateProfileString method requires a FileSystem path, meaning no PSDrive paths
$fullyQualifiedFilePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($GptIniPath)

[void][GPRegistryPolicyDsc.IniUtility]::WritePrivateProfileString(
$AppName,
$KeyName,
$KeyValue,
$fullyQualifiedFilePath
)
}

<#
.SYNOPSIS
Determines the incremented version number from the specified gpt.ini file.
.PARAMETER Version
The current gpt.ini version number which will be incremented based on TargetType.
.PARAMETER TargetType
Indicates the target type. This is needed to determine the gpt.ini file path. Supported values are LocalMachine, User, Administrators, NonAdministrators, Account.
#>
function Get-IncrementedGptVersion
{
[CmdletBinding()]
[OutputType([System.Int32])]
param
(
[Parameter(Mandatory = $true)]
[System.String]
$Version,

[Parameter(Mandatory = $true)]
[ValidateSet('ComputerConfiguration','UserConfiguration','Administrators','NonAdministrators','Account')]
[System.String]
$TargetType
)

<#
Reference: https://docs.microsoft.com/en-us/archive/blogs/grouppolicy/understanding-the-gpo-version-number
The version integer value in the GPT.ini has the following structure:
Version = [user version number top 16 bits] [computer version number lower 16 bits]
Below is a simple way to split the version number into the user and computer version number:
* First, recognize that the version number is in decimal. Before we can split the number into the two version numbers,
we first convert the decimal value to hex. The easiest way to perform this conversion is to use the calculator in windows
in scientific mode. Enter the decimal value and then click the hex button to convert the number. You should see a value of 15002F.
* If you are using the calculator, it will not display the leading zeros of the number. In hexadecimal, four hexadecimal characters
are equal to 16 bits. When you split the number into two parts you'll need to add two leading zeros to show the full version number
in hexadecimal. For our case, I would write this number out as 0015002F. (When written on paper, a 0x is added to the beginning of
the number to clarify the number is hexadecimal, 0x0015002F.)
* Input the lower 4 hex characters (002F) into the calculator while in hex mode. Then convert this value to decimal by clicking the
decimal button. You should see a computer version number of 47 decimal.
* Input the upper 4 hex characters (0015) into the calculator while in hex mode. Then convert this value to decimal by clicking the
decimal button. You should see a user version number of 21 decimal.
#>

# Increment gpt.ini version number based on user or computer policy change.
$versionBytes = [System.BitConverter]::GetBytes([int]$Version)
$loVersion = [System.BitConverter]::ToUInt16($versionBytes, 0)
$hiVersion = [System.BitConverter]::ToUInt16($versionBytes, 2)

if ($TargetType -eq 'ComputerConfiguration')
{
if ($loVersion -eq [uint16]::MaxValue)
{
# Once the GPT version hits the uint16 max (65535), the incremented number is reset to 1
$loVersion = 1
}
else
{
$loVersion++
}
}
else
{
if ($hiVersion -eq [uint16]::MaxValue)
{
# Once the GPT version hits the uint16 max (65535), the incremented number is reset to 1
$hiVersion = 1
}
else
{
$hiVersion++
}
}

# Convert lo/hi to byte array
$loVersionByte = [System.BitConverter]::GetBytes($loVersion)
$hiVersionByte = [System.BitConverter]::GetBytes($hiVersion)

# Create new byte array and convert to int32
$newGptVersionBytes = [byte[]]::new(4)
$newGptVersionBytes[0] = $loVersionByte[0]
$newGptVersionBytes[1] = $loVersionByte[1]
$newGptVersionBytes[2] = $hiVersionByte[0]
$newGptVersionBytes[3] = $hiVersionByte[1]

return [System.BitConverter]::ToInt32($newGptVersionBytes, 0)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ ConvertFrom-StringData -StringData @'
RetrievingCurrentState = Retrieving current for Key {0} ValueName {1}. (RPF04)
InDesiredState = Resource is in desired state. No refresh required. (RPF05)
AccountNameNull = No AccountName was provided. (RPF06)
GptIniCseUpdate = Gpt.ini {0} CSE GUID updated from {1} to {2}. (RPF07)
GptIniVersionUpdate = Gpt.ini Version updated based on {0} from {1} to {2}. (RPF08)
'@
2 changes: 1 addition & 1 deletion source/GPRegistryPolicyDsc.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@

PSData = @{
Prerelease = ''

# Tags applied to this module. These help with module discovery in online galleries.
Tags = @('DesiredStateConfiguration', 'DSC', 'DSCResourceKit', 'DSCResource')

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
$script:modulesFolderPath = Split-Path -Path $PSScriptRoot -Parent

<#
GetPrivateProfileString and WritePrivateProfileString are functions exposed via kernel32.dll that allow for reading and
creating/modifying .ini files respectively. These signatures are defined below and exposed when the module is imported
to be used in correctly configuring the gpt.ini file in order for Group Policy to be processed successfully.
Reference:
GetPrivateProfileString: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getprivateprofilestring
WritePrivateProfileString: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-writeprivateprofilestringa
#>

$profileStringSignature = @'
[DllImport("kernel32.dll")]
public static extern uint GetPrivateProfileString(
string lpAppName,
string lpKeyName,
string lpDefault,
StringBuilder lpReturnedString,
uint nSize,
string lpFileName
);
[DllImport("kernel32.dll")]
public static extern bool WritePrivateProfileString(
string lpAppName,
string lpKeyName,
string lpString,
string lpFileName
);
'@

Add-Type -MemberDefinition $profileStringSignature -Name IniUtility -Namespace GPRegistryPolicyDsc -Using System.Text

<#
.SYNOPSIS
This method is used to compare current and desired values for any DSC resource.
Expand Down
Loading

0 comments on commit 3f0c234

Please sign in to comment.