src/client/LocalProfile.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. . (import-script LocalProfileSpec) . (import-script LocalSettings) . (import-script LocalConnectionProfile) . (import-script ../cmdlets/New-GraphConnection) ScriptClass LocalProfile { $Name = $null $ConnectionProfile = $null $ProfileData = $null $ConnectionData = $null $EndpointData = $null $InitialApiVersion = $null $LogLevel = $null function __initialize([HashTable] $profileData, [HashTable] $connectionData, [HashTable] $endpointData) { $this.profileData = $profileData $this.connectionData = $connectionData $this.endpointData = $endpointData $this.name = $profileData.name $this.InitialApiVersion = $profileData['initialApiVersion'] $this.logLevel = $profileData['logLevel'] $endpointCollection = if ( $endpointData ) { @{$endpointData.Name=$endpointData} } $this.connectionProfile = new-so LocalConnectionProfile $connectionData $endpointCollection } function GetConnection { if ( $this.connectionProfile.Name ) { $::.GraphConnection |=> GetNamedConnection $this.connectionProfile.Name } } function ToConnectionParameters([string[]] $permissions) { $this.connectionProfile |=> ToConnectionParameters $permissions } function ToPublicProfile { $connectionName = if ( $this.connectionProfile ) { $this.connectionProfile.Name } $profileInfo = [ordered] @{} # Ensure these "well-known" values are part of the structure even # if they are not set -- other settings may come from external modules # that extend the profile structure with their own module-specific # properties $profileInfo['ProfileName'] = $this.name $profileInfo['Connection'] = $connectionName $profileInfo['IsDefault'] = $this.name -eq $this.scriptclass.defaultProfileName $profileInfo['AutoConnect'] = $this.profileData['AutoConnect'] $profileInfo['NoBrowserSigninUI'] = $this.profileData['noBrowserSigninUI'] $profileInfo['InitialApiVersion'] = $this.InitialApiVersion $profileInfo['LogLevel'] = $this.logLevel # Add these last so that we get the correct capitalization -- the setting names # may be camel-cased or have irregular casing -- we'll at least try to adjust # for the camel-case scenario by capitalizing the initial below if ( $this.profileData ) { foreach ( $property in $this.profileData.keys ) { if ( $property -ne 'name' -and ! $profileInfo.Contains($property) ) { $capitalized = $property[0].ToString().ToUpper() + $property.substring(1, $property.length - 1) $profileInfo[$capitalized] = $this.profileData[$property] } } } $result = [PSCustomObject] $profileInfo $result.pstypenames.insert(0, 'GraphProfileSettings') $result } function GetSetting([string] $settingName) { if ( $this.profileData ) { $this.profileData[$settingName] } } static { const defaultSettingsPath '~/.autographps/settings.json' $settings = $null $profiles = $null $defaultProfileName = $null $settingsLoadAttempted = $false $settingsPath = $null $settingsBypassed = $false $currentProfile = $null $connectionCommand = $null $propertyReaders = @{ name = @{ Validator = 'NameValidator'; Required = $true } initialApiVersion = @{ Validator = 'StringValidator'; Required = $false Updater = { $currentProfile = $::.LocalProfile |=> GetCurrentProfile if ( $currentProfile -and $currentProfile.InitialApiVersion ) { $currentContext = $::.GraphContext |=> GetCurrent # Only do this if we have a context -- if there is none, this must be startup, # so let this value be set by the rest of the startup process if ( $currentContext ) { $newContext = $::.LogicalGraphManager |=> Get |=> NewContext $currentContext $null $currentProfile.InitialApiVersion $null $false $::.GraphContext |=> SetCurrentByName $newContext.name } } } } $::.LocalProfileSpec.ConnectionProperty = @{ Validator = 'ConnectionValidator'; Required = $false Updater = { $currentProfile = $::.LocalProfile |=> GetCurrentProfile if ( $currentProfile ) { $connection = $currentProfile |=> GetConnection $currentContext = $::.GraphContext |=> GetCurrent if ( $connection -and $currentContext ) { $currentContext |=> UpdateConnection $connection } } } } noBrowserSigninUI = @{ Validator = 'BooleanValidator'; Required = $false } autoConnect = @{ Validator = 'BooleanValidator'; Required = $false } logLevel = @{ Validator = 'StringValidator'; Required = $false Updater = { $currentProfile = $::.LocalProfile |=> GetCurrentProfile if ( $currentProfile -and $currentProfile.logLevel ) { try { $logger = $::.RequestLog |=> GetDefault $logger.LogLevel = $currentProfile.loglevel } catch { } } } } } function __initialize($connectionCommand) { $this.settings = $null $this.profiles = $null $this.defaultProfileName = $null $this.settingsLoadAttempted = $false $this.settingsPath = $null $this.settingsBypassed = $false $this.currentProfile = $null $this.connectionCommand = $connectionCommand __RegisterSettingProperties } function LoadProfiles { if ( $this.settings ) { $this.settings |=> Load $settingData = __GetSettingData $endpointData = $settingData.endpointData $connectionData = $settingData.connectionData $profileData = $settingData.profileData # We need this strange workaround for scriptclass because scriptclass # hosts the code for static methods in a separate 'custom' module from the rest # of the class and the overall module itself. So commands exported # by this module are invisible (unless you dot source the module's code # instead of importing it as a module). So we inject the command script # as a parameter, assuming that the caller of __initialize is outside of # this custom module and has access to the New-GraphConnection command. # TODO: Find a better way to do this. :) Easiest fix would be to simply make an internal # version of New-GraphConnection exposed as a class method instead of using the command. new-item -force function:New-GraphConnection -value $this.connectionCommand | out-null if ( $connectionData ) { foreach ( $connectionElement in $connectionData.values ) { $connectionProfile = new-so LocalConnectionProfile $connectionElement $endpointData $connectionParameters = $connectionProfile |=> ToConnectionParameters write-verbose "Configuring application '$($connectionProfile.name)' with the following parameters:" foreach ( $parameterName in $connectionParameters.Keys ) { write-verbose ( "{0}: {1}" -f $parameterName, $connectionParameters[$parameterName] ) } try { New-GraphConnection @connectionParameters -Name $connectionProfile.Name | out-null } catch { write-warning "Unable to configure specified connection setting '$($connectionProfile.name)'" write-warning $_.exception.message } } } __UpdateProfileSettings $endpointData $connectionData $profileData $this.defaultProfileName = $this.settings |=> GetSettingValue defaultProfile if ( $this.defaultProfileName ) { SetCurrentProfile $this.defaultProfileName } $::.LocalSettings |=> RefreshBehaviorsFromSettings } } function ReloadProfileSettings([boolean] $resetAllSettings) { $settingData = __GetSettingData $currentProfile = $::.LocalProfile |=> GetCurrentProfile # This creates new profiles for each reloaded profile, so the current profile will be invalid __UpdateProfileSettings $settingData.endpointData $settingData.connectionData $settingData.profileData # Set reloaded current profile to be the current profile if ( $currentProfile ) { SetCurrentProfile $currentProfile.Name } $::.LocalSettings |=> RefreshBehaviorsFromSettings $resetAllSettings } function GetProfiles { __LoadProfiles $this.profiles } function GetProfileByName($profileName) { __GetProfileByName $profileName } function GetDefaultProfile { __LoadProfiles GetDefaultProfileFromSettings $this.settings $this.profiles } function GetCurrentProfile { $this.currentProfile } function GetValidatedSerializableSettings($settings) { # Settings have the following rules: # * A setting is a collection of key-value pairs called properties # * Keys are identified by a value of type string and the values may be of any type # * Within a given setting, keys must be unique # * Values may need to satisfy constraints to be considered valid # * There are three types of settings: profiles, connections, and endpoints # * Each of these kinds of settings requires a name property # * The name property must have a unique value across settings of a given kind # * In addition to the name property, each type of setting may have any number of additional properties # * Profile settings may specify a connection property that refers to the name property of a connection setting # * Connection settings my specify an endpoint property that refers to the name property of an endpoint setting # * Endpoint settings have no property that refers to any setting of any type # * There is an optional property with the name 'defaultProfile' associated with none of the setting types; its value is the value of one of the name properties of one of the profile settings. # Retrieve all settings -- properties are excluded from settings if the properties # are not valid (e.g. the values do not satisfy constraints). However, properties # that reference other settings are *not* validated. $settingData = __GetSettingDataFromSettings $settings # Now construct the internal representation of the profile -- this has the side effect # of evaluating cross-setting references and removes properties that make invalid references $profiles = __GetProfilesFromSettingData $settingData.endpointData $settingData.connectionData $settingData.profileData $validSettings = @{ defaultProfile = $null profiles = @{ list = @() } connections = @{ list = @() } endpoints = @{ list = @() } } # Return the profiles that have been validated as part of cross-setting # reference checks $profiles | foreach { $validSettings.profiles.list += $_.profileData } # Include all connections regardless if they are referenced by # a profile since connections may still be used outside the context # of a profile $settingData.connectionData.values | foreach { $validSettings.connections.list += $_ } # Include any endpoints, even if they are not referenced by connection # for completeness $settingData.endpointData.values | foreach { $validSettings.endpoints.list += $_ } $defaultProfile = GetDefaultProfileFromSettings $settings $profiles $validSettings.defaultProfile = if ( $defaultProfile ) { $defaultProfile.name } $validSettings | ConvertTo-Json -depth 5 | ConvertFrom-Json } function SetCurrentProfile($profileName, $refreshSettings) { $newProfile = __GetProfileByName $profileName if ( $newProfile ) { $this.currentProfile = $newProfile if ( $refreshSettings ) { $::.LocalSettings |=> RefreshBehaviorsFromSettings } } else { throw "Cannot set the current profile settings -- the specified profile '$profileName' could not be found." } } function GetDefaultProfileFromSettings($settings, $profiles) { $defaultProfileName = if ( $settings ) { $settings |=> GetSettingValue defaultProfile } if ( $defaultProfileName ) { $defaultProfile = if ( $profiles ) { $profiles | where name -eq $defaultProfileName } if ( ! $defaultProfile ) { $message = "The specified default profile name '$defaultProfileName' could not be found" if ( $settings.failOnErrors ) { throw $message } else { write-warning $message } } $defaultProfile } } function __GetProfileByName($profileName) { __GetProfileByNameFromProfiles $this.profiles $profileName } function __GetProfileByNameFromProfiles($profiles, $profileName) { if ( $profiles ) { $profiles | where name -eq $profileName } } function __LoadSettings($settingsPath) { if ( ! $this.settingsLoadAttempted -or $settingsPath ) { $this.settingsLoadAttempted = $true $this.settingsBypassed = ! $settingsPath -and ( test-path env:AUTOGRAPH_BYPASS_SETTINGS ) if ( ! $this.settingsBypassed ) { $targetPath = if ( $settingsPath ) { write-verbose "Settings load: Overriding default behaviors and environment variables with explicit path '$settingsPath'" $settingsPath } else { GetSettingsFileLocation } $this.settings = new-so LocalSettings $targetPath } } } function GetSettingsFileLocation { if ( test-path env:AUTOGRAPH_SETTINGS_FILE ) { $env:AUTOGRAPH_SETTINGS_FILE } else { $this.defaultSettingsPath } } function __LoadProfiles { __LoadSettings if ( $this.settings -and ! $this.profiles ) { LoadProfiles } } function __GetSettingData { __GetSettingDataFromSettings $this.settings } function __GetSettingDataFromSettings($settings) { $endpointData = @{} $connectionData = @{} $profileData = @{} if ( $settings ) { $endpointData = $settings |=> GetSettings $::.LocalProfileSpec.EndpointsCollection $connectionData = $settings |=> GetSettings $::.LocalProfileSpec.ConnectionsCollection $endpointData $profileData = $settings |=> GetSettings $::.LocalProfileSpec.ProfilesCollection $connectionData } @{ endpointData = $endpointData connectionData = $connectionData profileData = $profileData } } function __UpdateProfileSettings($endpointData, $connectionData, $profileData) { $this.profiles = __GetProfilesFromSettingData $endpointData $connectionData $profileData } function __GetProfilesFromSettingData($endpointData, $connectionData, $profileData) { if ( $profileData ) { foreach ( $profileDataItem in $profileData.values ) { $normalizedData = __GetNormalizedProfileData $profileDataItem $connectionData $endpointData new-so LocalProfile $normalizedData.ProfileData $normalizedData.ConnectionProfileData $normalizedData.EndpointData } } } function __GetNormalizedProfileData($profileData, $connections, $endpoints) { $referencedConnectionName = $profileData[$::.LocalProfileSpec.ConnectionProperty] $referencedEndpointName = $null $endpointData = $null $connectionData = $null if ( $connections -and $referencedConnectionName ) { $connectionData = $connections[$referencedConnectionName] if ( $connectionData ) { $endpointName = $connectionData[$::.LocalProfileSpec.EndpointProperty] $referencedEndpointName = if ( $endpointName -and ! ( $::.GraphEndpoint |=> IsWellKnownCloud $endpointName ) ) { $endpointName } $endpointData = if ( $endpoints -and $referencedEndpointName ) { $endpoints[$referencedEndpointName] } } } $profileName = $profileData['name'] if ( $referencedConnectionName -and ! $connectionData ) { throw "Profile '$profileName' refers to non-existent connection name '$connectionProfileName'" } if ( $referencedEndpointName -and ! $endpointData ) { throw "Connection profile '$referencedConnectionName' in profile '$profileName' refers to non-existent endpoint '$referencedEndpointName'" } @{ ProfileData = $profileData ConnectionProfileData = $connectionData EndpointData = $endpointData } } function __RegisterSettingProperties { $::.LocalSettings |=> RegisterSettingProperties $::.LocalProfileSpec.ProfilesCollection $this.propertyReaders } } } $::.LocalProfile |=> __initialize (get-command New-GraphConnection).ScriptBlock |