src/client/LocalSettings.ps1
# Copyright 2021, Adam Edwards # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ScriptClass LocalSettings { $settingsPath = $null $settingsData = $null $lastLoadError = $null $failOnErrors = $false $failOnWarnings = $false function __initialize($settingsPath, [boolean] $failOnErrors, [boolean] $failOnWarnings) { $this.failOnErrors = $failOnErrors $this.failOnWarnings = $failOnWarnings $this.settingsPath = $settingsPath } function Load([boolean] $skipIfHasData, $settingsContent ) { write-verbose "Attempting to load settings from path '$this.settingsPath' with skipIfHasData = '$skipIfHasData'" if ( $this.settingsData -and $skipIfHasData ) { return } $serializedContent = if ( $settingsContent -and $settingsContent -is [String] ) { write-verbose 'Serialized content was directly specified -- it will be interperted as JSON' $settingsContent } elseif ( $this.settingsPath -and ( test-path $this.settingsPath ) ) { try { get-content $this.settingsPath | out-string } catch { $this.lastLoadError = $_.exception write-verbose "Failed to read settings file at '$($this.settingsPath)'" write-verbose $_.exception if ( $this.failOnErrors ) { throw } } write-verbose "Successfully read AutoGraph settings file at '$($this.settingsPath)'" } $this.settingsData = if ( $serializedContent ) { try { $serializedContent | convertfrom-json } catch { $this.lastLoadError = $_.exception write-warning "Unable to load settings because the specified content could not be parsed as valid JSON" write-warning $_.exception if ( $this.failOnErrors ) { throw } } $this.lastLoadError = $null write-verbose 'Successfully loaded non-empty settings.' } elseif ( $settingsContent ) { if ( ( $settingsContent -is [HashTable] ) -or ( $settingsContent -is [PSCustomObject] ) ) { # We use the ConvertFrom-Json command to deserialize across settings implementation, # and it results in PSCustomObjects. To ensure consistency since the implementation # likely assumes there are only PSCustomObjects and no hash tables or other types, # and also to handle any nested structures that could theoretically be either hash # tables or objects, we repeat deserialization. So while inefficient, serializing # and then deserializing using the same serializer used for all settings processing # will ensure consistent behavior across loading, validation, and any other scenarios. $settingsContent | ConvertTo-Json -depth 5 | ConvertFrom-Json } } else { write-verbose "No settings were loaded because the specified path '$($this.settingsPath)' was not a valid path or no file could be accessed there and no deserialized content was specified." } } function GetSettingsLocation { $this.settingsPath } function GetSettingValue($settingName) { if ( $this.settingsData -and ( $this.settingsData | gm $settingName -erroraction Ignore ) ) { $this.settingsData.$settingName } } function GetSettings([string] $settingsType, $context) { if ( $this.failOnWarnings ) { $WarningPreference = 'stop' } if ( $this.settingsData ) { $settingsData = __ReadGroupData $settingsType __GetSettingsFromGroupData $settingsType $settingsData $context } } function __ReadGroupData($groupName) { $defaultItem = @{} $items = @{} $groupData = if ( $this.settingsData -and ( $this.settingsData | gm $groupName -erroraction Ignore ) ) { $this.settingsData.$groupName } if ( $groupData ) { $defaultItem = if ( $groupData | gm defaults -erroraction Ignore ) { $groupData.defaults } $list = if ( $groupData | gm list -erroraction ignore ) { $groupData.list } if ( $list ) { foreach ( $listItem in $list ) { if ( $listItem | gm name -erroraction ignore ) { if ( ! $items.ContainsKey($listItem.Name ) ) { $items.Add($listItem.name, $listItem) } else { write-warning "Duplicate setting '$($listItem.name)' found in settings file '$($this.settingsPath)', the duplicate will be ignored; the issue can be fixed by updating the settings file." } } else { write-warning "A setting was specified without a name property -- it will be ignored." } } } } @{ defaultItem = $defaultItem list = $items } } function __GetSettingsFromGroupData([string] $groupName, $groupData, $context) { $validSettings = @{} if ( $groupData ) { $defaultSetting = __GetDefaultSettingForGroup $groupName $groupData $context $settingInfo = $this.scriptclass |=> __GetSettingTypeInfo $groupName foreach ( $setting in $groupData.list.values ) { $validSetting = $defaultSetting.Clone() $newSetting = __GetValidSetting $groupName $setting $context if ( $newSetting -ne $null ) { foreach ( $propertyName in $newSetting.keys ) { $validSetting[$propertyName] = $newSetting[$propertyName] } } # Allow default settings to be added unless FailOnIInvalidProperty is set if ( ( $newSetting -ne $null ) -or ! $settingInfo.FailOnInvalidProperty ) { $validSettings.Add($validSetting['name'], $validSetting) } } } $validSettings } function __GetDefaultSettingForGroup($groupName, $groupData, $context) { if ( $groupData.defaultItem ) { __GetValidSetting $groupName $groupData.defaultItem $context $true } else { @{} } } function __SettingContainsProperties($setting) { $propertyCount = if ( $setting -is [HashTable] ) { $setting.Count } elseif ( $setting -is [PSCustomObject] ) { $propertyCount = $setting | get-member -membertype noteproperty | measure-object | select-object -expandproperty count } $setting -and $propertyCount } function __GetValidSetting($settingType, $setting, $context, $isDefault) { $validSetting = @{} $settingName = if ( ! $isDefault ) { $setting.name } else { @{} } $validations = $this.scriptclass |=> __GetPropertyReaders $settingType $settingInfo = $this.scriptclass |=> __GetSettingTypeInfo $settingType foreach ( $propertyName in $validations.keys ) { if ( $isDefault -and $propertyName -eq 'name' ) { continue } $validation = $validations[$propertyName] $propertyReaderScript = if ( $validation ) { $this.scriptclass |=> __GetPropertyReaderScript $validation } $propertyValue = if ( $setting | gm $propertyName -erroraction ignore ) { $newValue = $setting.$propertyName if ( $newValue -eq $null ) { write-warning "Property '$propertyName' had a `$null value which is not valid, it will be skipped" continue } $newValue } $result = if ( $validation -and ! ( ( $propertyValue -eq $null ) -and ! $validation.Required ) ) { . $propertyReaderScript $propertyValue $context } if ( $result ) { if ( $result.ContainsKey('Error') ) { write-warning "Property '$propertyName' of setting '$settingName' of type '$settingType' is invalid: $($result.error)" if ( $settingInfo.FailOnInvalidProperty -or ( $validation.Required -and ! $isDefault ) ) { write-warning "Setting '$settingName' of type '$settingType' will be ignored due to an invalid value for required property '$propertyName'" $validSetting = $null break } else { continue } } if ( ( $result.value -eq $null ) -and ( $validation.Required -and ! $isDefault ) ) { write-warning "Property '$propertyName' of setting '$settingName' of type '$settingType' is a required property but was not set -- the setting will be ignored" $validSetting = $null break } $validSetting.Add($propertyName, $result.Value) } } if ( $validSetting ) { $validSetting } } static { $propertyReaders = $null $settingTypeInfo = $null $updatedProperties = $null function __initialize { $this.propertyReaders = @{} $this.settingTypeInfo = @{} $this.updatedProperties = @{} } function RegisterSettingProperties([string] $settingType, [HashTable] $propertyReaders, $failOnInvalidProperty) { $settingTypePropertyReaders = $this.propertyReaders[$settingType] if ( ! $settingTypePropertyReaders ) { if ( $settingTypePropertyReaders -eq $null ) { $settingTypePropertyReaders = @{} $this.propertyReaders.Add($settingType, $settingTypePropertyReaders) $this.settingTypeInfo.Add($settingType, @{FailOnInvalidProperty=$failOnInvalidProperty}) $this.updatedProperties.Add($settingType, @{}) } } foreach ( $property in $propertyReaders.Keys ) { $settingTypePropertyReaders.Add($property, $propertyReaders[$property]) } } function RefreshBehaviorsFromSettings([boolean] $refreshExistingSettings) { if ( $refreshExistingSettings ) { __ClearUpdatedProperties } $updaters = __GetPropertyUpdaters foreach ( $updater in $updaters ) { $settingProperties = $this.updatedProperties[$updater.SettingType] if ( ! $settingProperties[$updater.PropertyName] ) { . $updater.Updater $this.updatedProperties[$updater.SettingType][$updater.PropertyName] = $updater.Updater } } } function __ClearUpdatedProperties { foreach ( $settingType in $this.updatedProperties.keys ) { $this.updatedProperties[$settingType].Clear() } } function __GetSettingTypeInfo($settingType) { $result = $this.settingTypeInfo[$settingType] if ( $result ) { $result } else { @{} } } function __GetPropertyReaders($settingType) { $result = $this.propertyReaders[$settingType] if ( $result ) { $result } else { @{} } } function __GetPropertyUpdaters { $settingTypes = $this.propertyReaders.keys foreach ( $settingType in $settingTypes ) { foreach ( $settingTypeReaders in $this.propertyReaders[$settingType] ) { foreach ( $propertyName in $settingTypeReaders.keys ) { $propertyReader = $settingTypeReaders[$propertyName] if ( $propertyReader['Updater'] ) { @{SettingType=$settingType;PropertyName=$propertyName;Updater=$propertyReader['Updater']} } } } } } function __GetPropertyReaderScript($propertyReader) { $propertyTypeReaders[$propertyReader.Validator] } $propertyTypeReaders = @{ UriValidator = { param($value, $context) try { @{Value = ([Uri] $value)} } catch { @{Error = "The specified URI is invalid"} } } StringValidator = { param($value, $context) if ( $value -isnot [string] ) { @{ Error = "Expecting a string, but received a $($value.gettype().tostring())" } } else { @{Value = [string] $value } } } StringArrayValidator = { param($value, $context) if ( $value -isnot [string[]] -and $value -isnot [object[]] ) { @{ Error = "Expecting a string array ([string[]]), but received a $($value.gettype().tostring())" } } else { @{Value = [string[]] $value } } } GuidStringValidator = { param($value, $context) $guidValue = if ( $value -isnot [guid] ) { try { @{Value = [guid] $value} } catch { } } if ( $guidValue ) { $guidValue } else { @{ Error = "Specified value '$value' is not a valid guid" } } } TenantValidator = { param($value, $context) $tenant = if ( $value -is [guid] ) { $value.tostring() } elseif ( $value -is [string] ) { # A valid tenant in domain form has at least one '.' char if ( $value.contains('.') ) { $value } else { try { ([guid] $value).tostring() } catch { } } } if ( $tenant ) { @{Value = $tenant} } else { @{Error = "Specified value '$value' is not a valid tenant identifier in domain or guid format" } } } CertificatePathValidator = { param($value, $context) if ( $value -like 'cert:*' -or $value -like '*.pfx' -or $value -like '*.pem' ) { @{ Value = $value } } else { @{ Error = "The specified certificate path '$value' is not a valid file system path or PowerShell cert: drive path" } } } NameValidator = { param($value, $context) if ( ! $value ) { @{ Error = "The 'name' property must be a non-empty string" } } else { @{Value = $value } } } BooleanValidator = { param($value, $context) if ( $value -isnot [boolean] ) { @{ Error = "Expecting a boolean, but received a $($value.gettype().tostring())" } } else { @{Value = [boolean] $value } } } AppCredentialValidator = { param($value, $context) $valueData = @{} 'tenantId', 'certificatePath', 'certificateName' | foreach { if ( $value | gm $_ -erroraction ignore ) { $valueData.Add($_, $value.$_) } } if ( ! $valueData['tenantId'] ) { @{ Error = "Application credentials value is missing required field 'tenantId'" } } else { @{ Value = $valueData } } } EndpointValidator = { param($value, $endpoints) $errorValue = if ( ! ( $::.GraphEndpoint |=> IsWellKnownCloud $value ) ) { $customEndpoint = $endpoints[$value] if ( ! $customEndpoint ) { @{ Error = "Unknown endpoint '$value' was specified" } } } if ( $errorValue ) { $errorValue } else { @{Value = $value} } } ConnectionValidator = { param($value, $connections) $connection = $connections[$value] if ( ! $connection ) { @{ Error = "Unknown connection '$value' was specified" } } else { @{Value = $value} } } } } } $::.LocalSettings |=> __initialize |