diff --git a/gems/aws-sdk-core/lib/aws-sdk-core.rb b/gems/aws-sdk-core/lib/aws-sdk-core.rb index f9685d2373b..8c8a00c7b28 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core.rb @@ -7,7 +7,7 @@ require_relative 'aws-sdk-core/deprecations' # credential providers - +require_relative 'aws-sdk-core/profile' require_relative 'aws-sdk-core/credential_provider' require_relative 'aws-sdk-core/refreshing_credentials' require_relative 'aws-sdk-core/assume_role_credentials' @@ -30,7 +30,6 @@ require_relative 'aws-sdk-core/plugins/bearer_authorization' # client modules - require_relative 'aws-sdk-core/client_stubs' require_relative 'aws-sdk-core/async_client_stubs' require_relative 'aws-sdk-core/eager_loader' @@ -45,24 +44,20 @@ require_relative 'aws-sdk-core/util' # resource classes - require_relative 'aws-sdk-core/resources/collection' # logging - require_relative 'aws-sdk-core/log/formatter' require_relative 'aws-sdk-core/log/param_filter' require_relative 'aws-sdk-core/log/param_formatter' # stubbing - require_relative 'aws-sdk-core/stubbing/empty_stub' require_relative 'aws-sdk-core/stubbing/data_applicator' require_relative 'aws-sdk-core/stubbing/stub_data' require_relative 'aws-sdk-core/stubbing/xml_error' # stubbing protocols - require_relative 'aws-sdk-core/stubbing/protocols/json' require_relative 'aws-sdk-core/stubbing/protocols/rest' require_relative 'aws-sdk-core/stubbing/protocols/rest_json' @@ -73,7 +68,6 @@ require_relative 'aws-sdk-core/stubbing/protocols/api_gateway' # protocols - require_relative 'aws-sdk-core/error_handler' require_relative 'aws-sdk-core/rest' require_relative 'aws-sdk-core/xml' @@ -82,21 +76,17 @@ require_relative 'aws-sdk-core/rpc_v2' # event stream - require_relative 'aws-sdk-core/binary' require_relative 'aws-sdk-core/event_emitter' # endpoint discovery - require_relative 'aws-sdk-core/endpoint_cache' # client metrics - require_relative 'aws-sdk-core/client_side_monitoring/request_metrics' require_relative 'aws-sdk-core/client_side_monitoring/publisher' # utilities - require_relative 'aws-sdk-core/arn' require_relative 'aws-sdk-core/arn_parser' require_relative 'aws-sdk-core/ec2_metadata' @@ -104,7 +94,7 @@ # dynamic endpoints require_relative 'aws-sdk-core/endpoints' -require_relative 'aws-sdk-core/plugins/signature_v4' +require_relative 'aws-sdk-core/plugins/signature_v4' # deprecated # defaults require_relative 'aws-defaults' diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/profile.rb b/gems/aws-sdk-core/lib/aws-sdk-core/profile.rb new file mode 100644 index 00000000000..30352e756c7 --- /dev/null +++ b/gems/aws-sdk-core/lib/aws-sdk-core/profile.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require_relative 'profile/profile' +require_relative 'profile/profile_file' +require_relative 'profile/profile_file_factory' +require_relative 'profile/profile_file_parser' +require_relative 'profile/profile_file_standardizer' +require_relative 'profile/profile_file_utils' diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/profile/profile.rb b/gems/aws-sdk-core/lib/aws-sdk-core/profile/profile.rb new file mode 100644 index 00000000000..934fea940bf --- /dev/null +++ b/gems/aws-sdk-core/lib/aws-sdk-core/profile/profile.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Aws + # @api private + class Profile + def initialize(name, properties = {}) + @name = name + @properties = {} + update_properties(properties) + end + + attr_reader :name, :properties + + def update_properties(new_values) + new_values.each do |key, value| + if @properties.key?(key) + puts "Warning: Duplicate property '#{key}' detected in profile #{@name}. "\ + 'One value will be ignored.' + end + @properties[key] = value + end + end + + # @api private + class Property + attr_reader :name, :value, :sub_properties + + def initialize(name, value) + @name = name + @value = value + @sub_properties = parse_sub_properties + end + + def to_h + if @sub_properties + @sub_properties.transform_values(&:to_h) + else + @value + end + end + + private + + def parse_sub_properties + return nil unless @value.start_with?("\n") + + sub_properties = {} + @value.split(/[\r\n]+/).each do |raw_sub_property_line| + next if ProfileFileUtils.empty_line?(raw_sub_property_line) + + left, right = ProfileFileUtils.parse_property_definition_line( + raw_sub_property_line, "in sub-property of #{@name}" + ) + next unless ProfileFileUtils.valid_identifier?(left) + + if sub_properties.key?(left) + puts "Warning: Duplicate sub-property '#{left}' detected in property '#{@name}'. "\ + 'The later one in the file will be used.' + end + + sub_properties[left] = Property.new(left, right) + end + sub_properties + end + end + end +end \ No newline at end of file diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/profile/profile_file.rb b/gems/aws-sdk-core/lib/aws-sdk-core/profile/profile_file.rb new file mode 100644 index 00000000000..5c003d67482 --- /dev/null +++ b/gems/aws-sdk-core/lib/aws-sdk-core/profile/profile_file.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Aws + # @api private + class ProfileFile + def initialize(config_profiles, credentials_profiles) + @profiles = merge_files(config_profiles, credentials_profiles) + .transform_values { |profile| profile.properties.to_h } + end + + attr_reader :profiles + + # def credentials(profile_name) + # return nil unless @profiles.key?(profile_name) + # + # properties = @profiles[profile_name].properties + # + # if properties.key?('role_arn') + # return AssumeRoleCredentials.new(profile_name, @profiles) + # elsif properties.key?('aws_access_key_id') + # aws_access_key_id = properties['aws_access_key_id'] + # aws_secret_access_key = properties['aws_secret_access_key'] + # aws_session_token = properties['aws_session_token'] + # + # raise "'aws_secret_access_key' was not specified in profile: #{profile_name}" unless aws_secret_access_key + # + # if aws_session_token + # return SessionCredentials.new(aws_access_key_id.value, aws_secret_access_key.value, aws_session_token.value) + # else + # return BasicCredentials.new(aws_access_key_id.value, aws_secret_access_key.value) + # end + # else + # return nil + # end + # end + # + # def region(profile_name) + # return nil unless @profiles.key?(profile_name) + # + # region = @profiles[profile_name].properties['region'] + # + # return nil unless region + # + # region.value + # end + + private + + def merge_files(config_file, credentials_file) + aggregate_file = config_file.dup + + credentials_file.each do |credentials_profile_name, credentials_profile| + if !aggregate_file.key?(credentials_profile_name) + aggregate_file[credentials_profile_name] = credentials_profile + else + puts "Warning: The profile '#{credentials_profile_name}' was found in both the configuration and " \ + "credentials configuration files. The properties will be merged, using the property in the credentials " \ + "file if there are duplicates." + + aggregate_file[credentials_profile_name].update_properties(credentials_profile.properties) + end + end + + aggregate_file + end + end +end \ No newline at end of file diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/profile/profile_file_factory.rb b/gems/aws-sdk-core/lib/aws-sdk-core/profile/profile_file_factory.rb new file mode 100644 index 00000000000..8a7194ab3b9 --- /dev/null +++ b/gems/aws-sdk-core/lib/aws-sdk-core/profile/profile_file_factory.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Aws + # @api private + class ProfileFileFactory + def create + raw_config_file = ProfileFileParser.new(configuration_file_contents).parse + raw_credentials_file = ProfileFileParser.new(credentials_file_contents).parse + + standardized_config_file = ProfileFileStandardizer.new(raw_config_file, :config).standardize + standardized_credentials_file = ProfileFileStandardizer.new(raw_credentials_file, :credentials).standardize + + ProfileFile.new(standardized_config_file, standardized_credentials_file) + end + + private + + def configuration_file_contents + load_file('AWS_CONFIG_FILE', File.join(user_home_directory, '.aws', 'config')) + end + + def credentials_file_contents + load_file('AWS_SHARED_CREDENTIALS_FILE', File.join(user_home_directory, '.aws', 'credentials')) + end + + def load_file(file_environment_variable, default_file_location) + file_location = ENV[file_environment_variable] + + if file_location.nil? + file_location = File.expand_path(default_file_location) + end + + if file_location =~ /^~(\/|#{Regexp.quote(File::SEPARATOR)}).*$/ + file_location = user_home_directory + file_location[1..-1] + end + + configuration_file = Pathname.new(file_location) + + return '' unless configuration_file.readable? + + configuration_file.read + end + + def user_home_directory + require 'byebug' + byebug + if ENV['HOME'] + ENV['HOME'] + elsif ENV['USERPROFILE'] + ENV['USERPROFILE'] + elsif ENV['HOMEDRIVE'] && ENV['HOMEPATH'] + ENV['HOMEDRIVE'] + ENV['HOMEPATH'] + else + Dir.home + end + end + end +end \ No newline at end of file diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/profile/profile_file_parser.rb b/gems/aws-sdk-core/lib/aws-sdk-core/profile/profile_file_parser.rb new file mode 100644 index 00000000000..d9006552e53 --- /dev/null +++ b/gems/aws-sdk-core/lib/aws-sdk-core/profile/profile_file_parser.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +module Aws + # @api private + class ProfileFileParser + def initialize(profile_file_contents) + @profile_file_contents = profile_file_contents + @current_line_number = 0 + end + + def parse + initialize_profiles + @profiles + end + + private + + def initialize_profiles + @profiles = {} + + @profile_file_contents.split(/[\r\n]+/).each do |line| + @current_line_number += 1 + next if ProfileFileUtils.empty_line?(line) || + ProfileFileUtils.comment_line?(line) + + if ProfileFileUtils.profile_line?(line) + read_profile_line(line) + elsif ProfileFileUtils.property_continuation_line?(line) + read_property_continuation_line(line) + else + read_property_definition_line(line) + end + end + end + + def read_profile_line(line) + line_without_comments = remove_trailing_comments(line, %w[# ;]) + line_without_whitespace = ProfileFileUtils.trim_whitespace(line_without_comments) + + unless line_without_whitespace[-1] == ']' + raise ArgumentError, + "Profile definition must end with ']' on line #{@current_line_number}" + end + + line_without_brackets = line_without_whitespace[1..-2] + + @current_profile = ProfileFileUtils.trim_whitespace(line_without_brackets) + @current_property = nil + + @profiles[@current_profile] ||= {} + end + + def read_property_definition_line(line) + unless @current_profile + raise ArgumentError, + "Expected a profile definition, found property on line #{@current_line_number}" + end + + line_without_comments = remove_trailing_comments(line, [' #', ' ;', "\t#", "\t;"]) + line_without_whitespace = ProfileFileUtils.trim_whitespace(line_without_comments) + + key, value = ProfileFileUtils.parse_property_definition_line( + line_without_whitespace, "on line #{@current_line_number}" + ) + @current_property = key + @profiles[@current_profile][key] = value + end + + def read_property_continuation_line(line) + unless @current_profile + raise ArgumentError, + "Expected a profile definition, found continuation on line #{@current_line_number}" + end + unless @current_property + raise ArgumentError, + "Expected a property definition, found continuation on line #{@current_line_number}" + end + + line = ProfileFileUtils.trim_whitespace(line) + profile_properties = @profiles[@current_profile] + current_property_value = profile_properties[@current_property] + new_property_value = "#{current_property_value}\n#{line}" + + profile_properties[@current_property] = new_property_value + end + + def remove_trailing_comments(line, *comment_patterns) + line[0...find_earliest_match(line, *comment_patterns)] + end + + def find_earliest_match(line, search_patterns) + earliest_location = line.length + + search_patterns.each do |pattern| + location = line.index(pattern) + earliest_location = [location, earliest_location].min if location + end + + earliest_location + end + end +end \ No newline at end of file diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/profile/profile_file_standardizer.rb b/gems/aws-sdk-core/lib/aws-sdk-core/profile/profile_file_standardizer.rb new file mode 100644 index 00000000000..2cb8f1d1763 --- /dev/null +++ b/gems/aws-sdk-core/lib/aws-sdk-core/profile/profile_file_standardizer.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +module Aws + # @api private + class ProfileFileStandardizer + def initialize(raw_file, file_type) + @raw_file = raw_file + @file_type = file_type + end + + def standardize + standardize_profile_file + @standardized_file + end + + private + + def standardize_profile_file + @standardized_file = {} + + @raw_file.each do |raw_profile_name, profile_properties| + raw_profile_name_has_profile_prefix = + raw_profile_name.start_with?('profile ') || + raw_profile_name.start_with?("profile\t") + + if @file_type == :config + if raw_profile_name_has_profile_prefix + standardized_profile_name = raw_profile_name['profile'.length..-1].strip + elsif raw_profile_name == 'default' + standardized_profile_name = 'default' + else + puts "Ignoring profile '#{raw_profile_name}' because it did not " \ + "start with 'profile ' and it was not 'default'." + next + end + else + standardized_profile_name = raw_profile_name + end + + unless ProfileFileUtils.valid_identifier?(standardized_profile_name) + puts "Ignoring profile '#{standardized_profile_name}' because it was " \ + 'not alphanumeric with dashes or underscores.' + next + end + + is_default_profile = standardized_profile_name == 'default' + seen_profile_before = @standardized_file.key?(standardized_profile_name) + + if @file_type == :config && is_default_profile && seen_profile_before + if !raw_profile_name_has_profile_prefix && @default_profile_in_standardized_file_has_profile_prefix + puts "Ignoring profile '[default]', because '[profile default]' was found in the same file." + next + elsif raw_profile_name_has_profile_prefix && !@default_profile_in_standardized_file_has_profile_prefix + puts "Dropping earlier-seen '[default]', because '[profile default]' was found in the same file." + @standardized_file.delete(standardized_profile_name) + end + end + + unless @standardized_file.key?(standardized_profile_name) + if is_default_profile && raw_profile_name_has_profile_prefix + @default_profile_in_standardized_file_has_profile_prefix = true + end + + @standardized_file[standardized_profile_name] = Profile.new(standardized_profile_name) + end + + standardized_properties = standardize_properties(standardized_profile_name, profile_properties) + + @standardized_file[standardized_profile_name].update_properties(standardized_properties) + end + end + + def standardize_properties(profile_name, raw_properties) + standardized_properties = {} + + raw_properties.each do |property_name, property_value| + unless ProfileFileUtils.valid_identifier?(property_name) + puts "Ignoring property '#{property_name}' in profile '#{profile_name}' " \ + 'because its name was not alphanumeric with dashes or underscores.' + next + end + + standardized_properties[property_name] = Aws::Profile::Property.new(property_name, property_value).to_h + end + + standardized_properties + end + end +end \ No newline at end of file diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/profile/profile_file_utils.rb b/gems/aws-sdk-core/lib/aws-sdk-core/profile/profile_file_utils.rb new file mode 100644 index 00000000000..2bb6f941d32 --- /dev/null +++ b/gems/aws-sdk-core/lib/aws-sdk-core/profile/profile_file_utils.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Aws + # @api private + class ProfileFileUtils + class << self + def parse_property_definition_line(line, location) + first_equals_location = line.index('=') + if first_equals_location.nil? + raise ArgumentError, + "Expected an '=' sign defining a property #{location}" + end + + key = trim_whitespace(line[0...first_equals_location]).downcase + raise ArgumentError, "Property did not have a name #{location}" if key.empty? + + value = trim_whitespace(line[first_equals_location + 1..-1]) + [key, value] + end + + def trim_whitespace(line) + return '' if line.empty? + + start_index = 0 + end_index = line.length - 1 + start_index += 1 while whitespace?(line[start_index]) + end_index -= 1 while whitespace?(line[end_index]) + line[start_index..end_index] + end + + def whitespace?(char) + [' ', "\t"].include?(char) + end + + def empty_line?(line) + !line.match(/^[\t ]*$/).nil? + end + + def comment_line?(line) + line.start_with?('#') || line.start_with?(';') + end + + def profile_line?(line) + line.start_with?('[') + end + + def property_continuation_line?(line) + line.start_with?(' ') || line.start_with?("\t") + end + + def valid_identifier?(line) + line.match(%r{^[A-Za-z0-9_\-/.%@:+]+$}) + end + end + end +end \ No newline at end of file diff --git a/gems/aws-sdk-core/spec/aws/ini_parser_spec.rb b/gems/aws-sdk-core/spec/aws/ini_parser_spec.rb index a1b453f8186..bf7ff9e2473 100644 --- a/gems/aws-sdk-core/spec/aws/ini_parser_spec.rb +++ b/gems/aws-sdk-core/spec/aws/ini_parser_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require_relative '../spec_helper' module Aws describe IniParser do diff --git a/gems/aws-sdk-core/spec/aws/profile/config-file-location-tests.json b/gems/aws-sdk-core/spec/aws/profile/config-file-location-tests.json new file mode 100644 index 00000000000..e73a85f9578 --- /dev/null +++ b/gems/aws-sdk-core/spec/aws/profile/config-file-location-tests.json @@ -0,0 +1,150 @@ +{ + "description": [ + "These are test descriptions that specify which files and profiles should be loaded based on the specified environment ", + "variables.", + "See 'file-location-tests.schema.json' for a description of this file's structure." + ], + + "tests": [ + { + "name": "User home is loaded from $HOME with highest priority on non-windows platforms.", + "environment": { + "HOME": "/home/user", + "USERPROFILE": "ignored", + "HOMEDRIVE": "ignored", + "HOMEPATH": "ignored" + }, + "languageSpecificHome": "ignored", + "platform": "linux", + "profile": "default", + "configLocation": "/home/user/.aws/config", + "credentialsLocation": "/home/user/.aws/credentials" + }, + + { + "name": "User home is loaded using language-specific resolution on non-windows platforms when $HOME is not set.", + "environment": { + "USERPROFILE": "ignored", + "HOMEDRIVE": "ignored", + "HOMEPATH": "ignored" + }, + "languageSpecificHome": "/home/user", + "platform": "linux", + "profile": "default", + "configLocation": "/home/user/.aws/config", + "credentialsLocation": "/home/user/.aws/credentials" + }, + + { + "name": "User home is loaded from $HOME with highest priority on windows platforms.", + "environment": { + "HOME": "C:\\users\\user", + "USERPROFILE": "ignored", + "HOMEDRIVE": "ignored", + "HOMEPATH": "ignored" + }, + "languageSpecificHome": "ignored", + "platform": "windows", + "profile": "default", + "configLocation": "C:\\users\\user\\.aws\\config", + "credentialsLocation": "C:\\users\\user\\.aws\\credentials" + }, + + { + "name": "User home is loaded from $USERPROFILE on windows platforms when $HOME is not set.", + "environment": { + "USERPROFILE": "C:\\users\\user", + "HOMEDRIVE": "ignored", + "HOMEPATH": "ignored" + }, + "languageSpecificHome": "ignored", + "platform": "windows", + "profile": "default", + "configLocation": "C:\\users\\user\\.aws\\config", + "credentialsLocation": "C:\\users\\user\\.aws\\credentials" + }, + + { + "name": "User home is loaded from $HOMEDRIVE$HOMEPATH on windows platforms when $HOME and $USERPROFILE are not set.", + "environment": { + "HOMEDRIVE": "C:", + "HOMEPATH": "\\users\\user" + }, + "languageSpecificHome": "ignored", + "platform": "windows", + "profile": "default", + "configLocation": "C:\\users\\user\\.aws\\config", + "credentialsLocation": "C:\\users\\user\\.aws\\credentials" + }, + + { + "name": "User home is loaded using language-specific resolution on windows platforms when no environment variables are set.", + "environment": { + }, + "languageSpecificHome": "C:\\users\\user", + "platform": "windows", + "profile": "default", + "configLocation": "C:\\users\\user\\.aws\\config", + "credentialsLocation": "C:\\users\\user\\.aws\\credentials" + }, + + { + "name": "The default config location can be overridden by the user on non-windows platforms.", + "environment": { + "AWS_CONFIG_FILE": "/other/path/config", + "HOME": "/home/user" + }, + "platform": "linux", + "configLocation": "/other/path/config", + "credentialsLocation": "/home/user/.aws/credentials" + }, + + { + "name": "The default credentials location can be overridden by the user on non-windows platforms.", + "environment": { + "AWS_SHARED_CREDENTIALS_FILE": "/other/path/credentials", + "HOME": "/home/user" + }, + "platform": "linux", + "profile": "default", + "configLocation": "/home/user/.aws/config", + "credentialsLocation": "/other/path/credentials" + }, + + { + "name": "The default credentials location can be overridden by the user on windows platforms.", + "environment": { + "AWS_CONFIG_FILE": "C:\\other\\path\\config", + "HOME": "C:\\users\\user" + }, + "platform": "windows", + "profile": "default", + "configLocation": "C:\\other\\path\\config", + "credentialsLocation": "C:\\users\\user\\.aws\\credentials" + }, + + { + "name": "The default credentials location can be overridden by the user on windows platforms.", + "environment": { + "AWS_SHARED_CREDENTIALS_FILE": "C:\\other\\path\\credentials", + "HOME": "C:\\users\\user" + }, + "platform": "windows", + "profile": "default", + "configLocation": "C:\\users\\user\\.aws\\config", + "credentialsLocation": "C:\\other\\path\\credentials" + }, + + { + "name": "The default profile can be overridden via environment variable.", + "environment": { + "AWS_PROFILE": "other", + "HOME": "/home/user" + }, + "platform": "linux", + "profile": "other", + "configLocation": "/home/user/.aws/config", + "credentialsLocation": "/home/user/.aws/credentials" + } + ] +} \ No newline at end of file diff --git a/gems/aws-sdk-core/spec/aws/profile/config-file-parser-tests.json b/gems/aws-sdk-core/spec/aws/profile/config-file-parser-tests.json new file mode 100644 index 00000000000..e8b998f3713 --- /dev/null +++ b/gems/aws-sdk-core/spec/aws/profile/config-file-parser-tests.json @@ -0,0 +1,849 @@ +{ + "description": [ + "These are test descriptions that describe how to convert a raw configuration and credentials file into an ", + "in-memory representation of the profile file.", + "See 'parser-tests.schema.json' for a description of this file's structure." + ], + + "tests": [ + { + "name": "Empty files have no profiles.", + "input": { + "configFile" : "" + }, + "output": { + "profiles": {} + } + }, + + { + "name": "Empty profiles have no properties.", + "input": { + "configFile": "[profile foo]\n" + }, + "output": { + "profiles": { + "foo": {} + } + } + }, + + { + "name": "Profile definitions must end with brackets.", + "input": { + "configFile": "[profile foo" + }, + "output": { + "errorContaining": "Profile definition must end with ']'" + } + }, + + { + "name": "Profile names should be trimmed.", + "input": { + "configFile": "[profile \tfoo \t]" + }, + "output": { + "profiles": { + "foo": {} + } + } + }, + + { + "name": "Tabs can separate profile names from profile prefix.", + "input": { + "configFile": "[profile\tfoo]" + }, + "output": { + "profiles": { + "foo": {} + } + } + }, + + { + "name": "Properties must be defined in a profile.", + "input": { + "configFile": "name = value" + }, + "output": { + "errorContaining": "Expected a profile definition" + } + }, + + { + "name": "Profiles can contain properties.", + "input": { + "configFile": "[profile foo]\nname = value" + }, + "output": { + "profiles": { + "foo": { + "name": "value" + } + } + } + }, + + { + "name": "Windows style line endings are supported.", + "input": { + "configFile": "[profile foo]\r\nname = value" + }, + "output": { + "profiles": { + "foo": { + "name": "value" + } + } + } + }, + + { + "name": "Equals signs are supported in property values.", + "input": { + "configFile": "[profile foo]\nname = val=ue" + }, + "output": { + "profiles": { + "foo": { + "name": "val=ue" + } + } + } + }, + + { + "name": "Unicode characters are supported in property values.", + "input": { + "configFile": "[profile foo]\nname = 😂" + }, + "output": { + "profiles": { + "foo": { + "name": "😂" + } + } + } + }, + + { + "name": "Profiles can contain multiple properties.", + "input": { + "configFile": "[profile foo]\nname = value\nname2 = value2" + }, + "output": { + "profiles": { + "foo": { + "name": "value", + "name2": "value2" + } + } + } + }, + + { + "name": "Profiles can contain multiple properties.", + "input": { + "configFile": "[profile foo]\nname = value\nname2 = value2" + }, + "output": { + "profiles": { + "foo": { + "name": "value", + "name2": "value2" + } + } + } + }, + + { + "name": "Property keys and values are trimmed.", + "input": { + "configFile": "[profile foo]\nname \t= \tvalue \t" + }, + "output": { + "profiles": { + "foo": { + "name": "value" + } + } + } + }, + + { + "name": "Property values can be empty.", + "input": { + "configFile": "[profile foo]\nname =" + }, + "output": { + "profiles": { + "foo": { + "name": "" + } + } + } + }, + + { + "name": "Property key cannot be empty.", + "input": { + "configFile": "[profile foo]\n= value" + }, + "output": { + "errorContaining": "Property did not have a name" + } + }, + + { + "name": "Property definitions must contain an equals sign.", + "input": { + "configFile": "[profile foo]\nkey : value" + }, + "output": { + "errorContaining": "Expected an '=' sign defining a property" + } + }, + + { + "name": "Multiple profiles can be empty.", + "input": { + "configFile": "[profile foo]\n[profile bar]" + }, + "output": { + "profiles": { + "foo": {}, + "bar": {} + } + } + }, + + { + "name": "Multiple profiles can have properties.", + "input": { + "configFile": "[profile foo]\nname = value\n[profile bar]\nname2 = value2" + }, + "output": { + "profiles": { + "foo": { + "name": "value" + }, + "bar": { + "name2": "value2" + } + } + } + }, + + { + "name": "Blank lines are ignored.", + "input": { + "configFile": "\t \n[profile foo]\n\t\n \nname = value\n\t \n[profile bar]\n \t" + }, + "output": { + "profiles": { + "foo": { + "name": "value" + }, + "bar": {} + } + } + }, + + { + "name": "Pound sign comments are ignored.", + "input": { + "configFile": "# Comment\n[profile foo] # Comment\nname = value # Comment with # sign" + }, + "output": { + "profiles": { + "foo": { + "name": "value" + } + } + } + }, + + { + "name": "Semicolon sign comments are ignored.", + "input": { + "configFile": "; Comment\n[profile foo] ; Comment\nname = value ; Comment with ; sign" + }, + "output": { + "profiles": { + "foo": { + "name": "value" + } + } + } + }, + { + "name": "All comment types can be used together.", + "input": { + "configFile": "# Comment\n[profile foo] ; Comment\nname = value # Comment with ; sign" + }, + "output": { + "profiles": { + "foo": { + "name": "value" + } + } + } + }, + + { + "name": "Comments can be empty.", + "input": { + "configFile": ";\n[profile foo];\nname = value ;\n" + }, + "output": { + "profiles": { + "foo": { + "name": "value" + } + } + } + }, + + { + "name": "Comments can be adjacent to profile names.", + "input": { + "configFile": "[profile foo]; Adjacent semicolons\n[profile bar]# Adjacent pound signs" + }, + "output": { + "profiles": { + "foo": {}, + "bar": {} + } + } + }, + + { + "name": "Comments adjacent to values are included in the value.", + "input": { + "configFile": "[profile foo]\nname = value; Adjacent semicolons\nname2 = value# Adjacent pound signs" + }, + "output": { + "profiles": { + "foo": { + "name": "value; Adjacent semicolons", + "name2": "value# Adjacent pound signs" + } + } + } + }, + + { + "name": "Property values can be continued on the next line.", + "input": { + "configFile": "[profile foo]\nname = value\n -continued" + }, + "output": { + "profiles": { + "foo": { + "name": "value\n-continued" + } + } + } + }, + + { + "name": "Property values can be continued with multiple lines.", + "input": { + "configFile": "[profile foo]\nname = value\n -continued\n -and-continued" + }, + "output": { + "profiles": { + "foo": { + "name": "value\n-continued\n-and-continued" + } + } + } + }, + + { + "name": "Continuations are trimmed.", + "input": { + "configFile": "[profile foo]\nname = value\n \t -continued \t " + }, + "output": { + "profiles": { + "foo": { + "name": "value\n-continued" + } + } + } + }, + + { + "name": "Continuation values include pound comments.", + "input": { + "configFile": "[profile foo]\nname = value\n -continued # Comment" + }, + "output": { + "profiles": { + "foo": { + "name": "value\n-continued # Comment" + } + } + } + }, + + { + "name": "Continuation values include semicolon comments.", + "input": { + "configFile": "[profile foo]\nname = value\n -continued ; Comment" + }, + "output": { + "profiles": { + "foo": { + "name": "value\n-continued ; Comment" + } + } + } + }, + + { + "name": "Continuations cannot be used outside of a profile.", + "input": { + "configFile": " -continued" + }, + "output": { + "errorContaining": "Expected a profile definition" + } + }, + + { + "name": "Continuations cannot be used outside of a property.", + "input": { + "configFile": "[profile foo]\n -continued" + }, + "output": { + "errorContaining": "Expected a property definition" + } + }, + + { + "name": "Continuations reset with profile definitions.", + "input": { + "configFile": "[profile foo]\nname = value\n[profile foo]\n -continued" + }, + "output": { + "errorContaining": "Expected a property definition" + } + }, + + { + "name": "Duplicate profiles in the same file merge properties.", + "input": { + "configFile": "[profile foo]\nname = value\n[profile foo]\nname2 = value2" + }, + "output": { + "profiles": { + "foo": { + "name": "value", + "name2": "value2" + } + } + } + }, + + { + "name": "Duplicate properties in a profile use the last one defined.", + "input": { + "configFile": "[profile foo]\nname = value\nname = value2" + }, + "output": { + "profiles": { + "foo": { + "name": "value2" + } + } + } + }, + + { + "name": "Duplicate properties in duplicate profiles use the last one defined.", + "input": { + "configFile": "[profile foo]\nname = value\n[profile foo]\nname = value2" + }, + "output": { + "profiles": { + "foo": { + "name": "value2" + } + } + } + }, + + { + "name": "Default profile with profile prefix overrides default profile without prefix when profile prefix is first.", + "input": { + "configFile": "[profile default]\nname = value\n[default]\nname2 = value2" + }, + "output": { + "profiles": { + "default": { + "name": "value" + } + } + } + }, + + { + "name": "Default profile with profile prefix overrides default profile without prefix when profile prefix is last.", + "input": { + "configFile": "[default]\nname2 = value2\n[profile default]\nname = value" + }, + "output": { + "profiles": { + "default": { + "name": "value" + } + } + } + }, + + { + "name": "Invalid profile names are ignored.", + "input": { + "configFile": "[profile in valid]\nname = value", + "credentialsFile": "[in valid 2]\nname2 = value2" + }, + "output": { + "profiles": {} + } + }, + + { + "name": "Invalid property names are ignored.", + "input": { + "configFile": "[profile foo]\nin valid = value" + }, + "output": { + "profiles": { + "foo": {} + } + } + }, + + { + "name": "All valid profile name characters are supported.", + "input": { + "configFile": "[profile ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-/.%@:+]" + }, + "output": { + "profiles": { + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-/.%@:+": {} + } + } + }, + + { + "name": "All valid property name characters are supported.", + "input": { + "configFile": "[profile foo]\nABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-/.%@:+ = value" + }, + "output": { + "profiles": { + "foo": { + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz0123456789_-/.%@:+": "value" + } + } + } + }, + + { + "name": "Properties can have sub-properties.", + "input": { + "configFile": "[profile foo]\ns3 =\n name = value" + }, + "output": { + "profiles": { + "foo": { + "s3": { + "name": "value" + } + } + } + } + }, + + { + "name": "Invalid sub-property definitions cause an error.", + "input": { + "configFile": "[profile foo]\ns3 =\n invalid" + }, + "output": { + "errorContaining": "Expected an '=' sign defining a property in sub-property" + } + }, + + { + "name": "Sub-property definitions can have an empty value.", + "input": { + "configFile": "[profile foo]\ns3 =\n name =" + }, + "output": { + "profiles": { + "foo": { + "s3": { + "name": "" + } + } + } + } + }, + + { + "name": "Sub property definitions have pound comments applied to the value.", + "input": { + "configFile": "[profile foo]\ns3 =\n name = value # Comment" + }, + "output": { + "profiles": { + "foo": { + "s3": { + "name": "value # Comment" + } + } + } + } + }, + + { + "name": "Sub property definitions have semicolon comments applied to the value.", + "input": { + "configFile": "[profile foo]\ns3 =\n name = value ; Comment" + }, + "output": { + "profiles": { + "foo": { + "s3": { + "name": "value ; Comment" + } + } + } + } + }, + + { + "name": "Sub-property definitions cannot have an empty name.", + "input": { + "configFile": "[profile foo]\ns3 =\n = value" + }, + "output": { + "errorContaining": "Property did not have a name in sub-property" + } + }, + + { + "name": "Sub-property definitions cannot have an invalid name.", + "input": { + "configFile": "[profile foo]\ns3 =\n in valid = value" + }, + "output": { + "profiles": { + "foo": { + "s3": {} + } + } + } + }, + + { + "name": "Sub-properties can have blank lines that are ignored", + "input": { + "configFile": "[profile foo]\ns3 =\n name = value\n\t \n name2 = value2" + }, + "output": { + "profiles": { + "foo": { + "s3": { + "name": "value", + "name2": "value2" + } + } + } + } + }, + + { + "name": "Profiles duplicated in multiple files are merged.", + "input": { + "configFile": "[profile foo]\nname = value", + "credentialsFile": "[foo]\nname2 = value2" + }, + "output": { + "profiles": { + "foo": { + "name": "value", + "name2": "value2" + } + } + } + }, + + { + "name": "Default profiles with mixed prefixes in the config file ignore the one without prefix when merging.", + "input": { + "configFile": "[profile default]\nname = value\n[default]\nname2 = value2\n[profile default]\nname3 = value3" + }, + "output": { + "profiles": { + "default": { + "name": "value", + "name3": "value3" + } + } + } + }, + + { + "name": "Default profiles with mixed prefixes merge with credentials", + "input": { + "configFile": "[profile default]\nname = value\n[default]\nname2 = value2\n[profile default]\nname3 = value3", + "credentialsFile": "[default]\nsecret=foo" + }, + "output": { + "profiles": { + "default": { + "name": "value", + "name3": "value3", + "secret": "foo" + } + } + } + }, + + { + "name": "Duplicate properties between files uses credentials property.", + "input": { + "configFile": "[profile foo]\nname = value", + "credentialsFile": "[foo]\nname = value2" + }, + "output": { + "profiles": { + "foo": { + "name": "value2" + } + } + } + }, + + { + "name": "Config profiles without prefix are ignored.", + "input": { + "configFile": "[foo]\nname = value" + }, + "output": { + "profiles": {} + } + }, + + { + "name": "Credentials profiles with prefix are ignored.", + "input": { + "credentialsFile": "[profile foo]\nname = value" + }, + "output": { + "profiles": {} + } + }, + + { + "name": "Comment characters adjacent to profile decls", + "input": { + "configFile": "[profile foo]; semicolon\n[profile bar]# pound" + }, + "output": { + "profiles": { + "foo": {}, + "bar": {} + } + } + }, + + { + "name": "Invalid continuation", + "input": { + "configFile": "[profile foo]\nname = value\n[profile foo]\n -continued" + }, + "output": { + "errorContaining": "Expected a property definition, found continuation" + } + }, + + { + "name": "profile name with no space after `profile` is invalid", + "input": { + "configFile": "[profilefoo]\nname = value\n[profile bar]" + }, + "output": { + "profiles": { + "bar": {} + } + } + }, + + { + "name": "profile name with extra whitespace", + "input": { + "configFile": "[ profile foo ]\nname = value\n[profile bar]" + }, + "output": { + "profiles": { + "bar": {}, + "foo": { + "name": "value" + } + } + } + }, + + { + "name": "profile name with extra whitespace in credentials", + "input": { + "credentialsFile": "[ foo ]\nname = value\n[profile bar]" + }, + "output": { + "profiles": { + "foo": { + "name": "value" + } + } + } + }, + + { + "name": "properties from an invalid profile name are ignored", + "input": { + "configFile": "[profile foo]\nname = value\n[profile in valid]\nx = 1\n[profile bar]\nname = value2" + }, + "output": { + "profiles": { + "bar": { + "name": "value2" + }, + "foo": { + "name": "value" + } + } + } + }, + { + "name": "Duplicate properties in duplicate profiles use the last one defined (case insensitive).", + "input": { + "configFile": "[profile foo]\nName = value\n[profile foo]\nname = value2" + }, + "output": { + "profiles": { + "foo": { + "name": "value2" + } + } + } + } + ] +} \ No newline at end of file diff --git a/gems/aws-sdk-core/spec/aws/profile/config-file-profile-tests.json b/gems/aws-sdk-core/spec/aws/profile/config-file-profile-tests.json new file mode 100644 index 00000000000..994af529ee2 --- /dev/null +++ b/gems/aws-sdk-core/spec/aws/profile/config-file-profile-tests.json @@ -0,0 +1,62 @@ +{ + "description": [ + "These are test descriptions that describe how specific data should be loaded from a profile file based on a ", + "profile name.", + "See 'profile-tests.schema.json' for a description of this file's structure." + ], + "testSuites": [ + { + "profiles": { + "default": { + "aws_access_key_id": "123", + "aws_secret_access_key": "456" + }, + "session": { + "aws_access_key_id": "123", + "aws_secret_access_key": "456", + "aws_session_token": "789" + }, + "role": { + "role_arn": "arn:aws:iam::123456789:role/MyRole", + "source_profile": "default", + "aws_access_key_id": "should-be-ignored", + "aws_secret_access_key": "should-be-ignored", + "aws_session_token": "should-be-ignored" + }, + "region": { + "region": "us-east-1" + } + }, + "regionTests": { + "name": "Regions can be loaded from profiles", + "profile": "region", + "output": { + "region": "us-east-1" + } + }, + "credentialsTests": [ + { + "name": "Role credentials have highest priority", + "profile": "role", + "output": { + "credentialType": "assumeRole" + } + }, + { + "name": "Session credentials have next highest priority", + "profile": "session", + "output": { + "credentialType": "session" + } + }, + { + "name": "Basic credentials have lowest priority", + "profile": "default", + "output": { + "credentialType": "basic" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/gems/aws-sdk-core/spec/aws/profile/location_tests_spec.rb b/gems/aws-sdk-core/spec/aws/profile/location_tests_spec.rb new file mode 100644 index 00000000000..b059739552c --- /dev/null +++ b/gems/aws-sdk-core/spec/aws/profile/location_tests_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require_relative '../../spec_helper' + +module Aws + describe ProfileFileFactory do + subject { described_class } + + context '#create' do + file = File.join(File.dirname(__FILE__),'config-file-location-tests.json') + test_cases = JSON.load_file(file)['tests'] + + def with_env(env, &block) + env.each do |key, value| + ENV[key] = value + end + block.call + env.each do |key, _| + ENV.delete(key) + end + end + + test_cases.each do |test_case| + it "passes: #{test_case['name']}" do + allow(Dir).to receive(:home).and_return(test_case['languageSpecificHome']) + expect(Pathname).to receive(:new).with(test_case['configLocation']).and_return(double(readable?: true, read: '')) + expect(Pathname).to receive(:new).with(test_case['credentialsLocation']).and_return(double(readable?: true, read: '')) + factory = subject.new + with_env(test_case['environment']) do + factory.create + end + end + end + end + end +end \ No newline at end of file diff --git a/gems/aws-sdk-core/spec/aws/profile/parser_tests_spec.rb b/gems/aws-sdk-core/spec/aws/profile/parser_tests_spec.rb new file mode 100644 index 00000000000..efeaaf7471a --- /dev/null +++ b/gems/aws-sdk-core/spec/aws/profile/parser_tests_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require_relative '../../spec_helper' + +module Aws + describe ProfileFile do + subject { described_class } + + context '#profiles' do + file = File.join(File.dirname(__FILE__),'config-file-parser-tests.json') + test_cases = JSON.load_file(file)['tests'] + + def parse(input) + config_file = input['configFile'] + credentials_file = input['credentialsFile'] + if config_file + raw_config_file = ProfileFileParser.new(config_file).parse + standardized_config_file = ProfileFileStandardizer.new(raw_config_file, :config).standardize + end + if credentials_file + raw_credentials_file = ProfileFileParser.new(credentials_file).parse + standardized_credentials_file = ProfileFileStandardizer.new(raw_credentials_file, :credentials).standardize + end + subject.new(standardized_config_file || {}, standardized_credentials_file || {}).profiles + end + + test_cases.each do |test_case| + it "passes: #{test_case['name']}" do + expected = test_case['output'] + + if expected.key?('profiles') + actual = parse(test_case['input']) + expect(actual).to eq(expected['profiles']) + elsif expected.key?('errorContaining') + expect { parse(test_case['input']) } + .to raise_error( + ArgumentError, include(expected['errorContaining']) + ) + end + end + end + end + end +end \ No newline at end of file diff --git a/gems/aws-sdk-core/spec/shared_spec_helper.rb b/gems/aws-sdk-core/spec/shared_spec_helper.rb index 70dd39ab60c..cc223346348 100644 --- a/gems/aws-sdk-core/spec/shared_spec_helper.rb +++ b/gems/aws-sdk-core/spec/shared_spec_helper.rb @@ -21,10 +21,12 @@ # Module to help check service signing config.include Sigv4Helper - config.before(:each) do + config.before(:all) do # Clear the current ENV to avoid loading credentials. ENV.keep_if { |k, _| k == 'PATH' } + end + config.before(:each) do # disable loading credentials from shared file allow(Dir).to receive(:home).and_raise(ArgumentError)