# typed: false # frozen_string_literal: true require 'aws-sigv4' require 'workato/utilities/xml' using Workato::Extension::HashWithIndifferentAccess module Workato module Connector module Sdk module Dsl module AWS TEMP_CREDENTIALS_REFRESH_TIMEOUT = 60 # seconds DUMMY_AWS_IAM_EXTERNAL_ID = 'dummy-aws-iam-external-id' DUMMY_AWS_WORKATO_ACCOUNT_ID = 'dummy-aws-workato-account-id' AMAZON_ROLE_CLIENT_ID = ENV.fetch('AMAZON_ROLE_CLIENT_ID', nil) AMAZON_ROLE_CLIENT_KEY = ENV.fetch('AMAZON_ROLE_CLIENT_KEY', nil) AMAZON_ROLE_CLIENT_SECRET = ENV.fetch('AMAZON_ROLE_CLIENT_SECRET', nil) WWW_FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded; charset=utf-8' def aws @aws ||= Private.new(connection: connection) end class Private def initialize(connection:) @connection = connection end def generate_signature(connection:, service:, region:, host: "#{service}.#{region}.amazonaws.com", path: '/', method: 'GET', params: {}, headers: {}, payload: '') credentials = if connection[:aws_assume_role].present? role_based_auth(settings: connection) else { access_key_id: connection[:aws_api_key], secret_access_key: connection[:aws_secret_key] } end url, headers = create_signature( credentials: credentials, service: service, host: host, region: region, method: method, path: path, params: params, headers: HashWithIndifferentAccess.wrap(headers), payload: payload ) { url: url, headers: headers }.with_indifferent_access end def iam_external_id @connection.settings[:aws_external_id] || DUMMY_AWS_IAM_EXTERNAL_ID end def workato_account_id @connection.settings[:aws_workato_account_id] || AMAZON_ROLE_CLIENT_ID || DUMMY_AWS_WORKATO_ACCOUNT_ID end private def role_based_auth(settings:) temp_credentials = settings[:temp_credentials] || @connection.settings[:temp_credentials] || {} # Refresh temp token that will expire within 60 seconds. expiration = temp_credentials[:expiration]&.to_time(:utc) if !expiration || expiration <= TEMP_CREDENTIALS_REFRESH_TIMEOUT.seconds.from_now @connection.update_settings!('Refresh AWS temporary credentials') do { temp_credentials: refresh_temp_credentials(settings) } end temp_credentials = @connection.settings[:temp_credentials] end { access_key_id: temp_credentials[:api_key], secret_access_key: temp_credentials[:secret_key], session_token: temp_credentials[:session_token] } end def refresh_temp_credentials(settings) aws_external_id = settings[:aws_external_id] || iam_external_id sts_credentials = { access_key_id: amazon_role_client_key(settings), secret_access_key: amazon_role_client_secret(settings) } sts_params = { 'Version' => '2011-06-15', 'Action' => 'AssumeRole', 'RoleSessionName' => 'workato', 'RoleArn' => settings[:aws_assume_role], 'ExternalId' => aws_external_id.presence }.compact sts_auth_url, sts_auth_headers = create_signature( credentials: sts_credentials, params: sts_params, service: 'sts', host: 'sts.amazonaws.com', region: 'us-east-1', headers: { 'Accept' => 'application/xml', 'Content-Type' => WWW_FORM_CONTENT_TYPE } ) request_temp_credentials(url: sts_auth_url, headers: sts_auth_headers) rescue StandardError => e raise e if aws_external_id.blank? aws_external_id = nil retry end def request_temp_credentials(url:, headers:) response = RestClient::Request.execute( url: url, headers: headers, method: :get ) response = Workato::Utilities::Xml.parse_xml_to_hash(response.body) temp_credentials = response.dig('AssumeRoleResponse', 0, 'AssumeRoleResult', 0, 'Credentials', 0) { session_token: temp_credentials.dig('SessionToken', 0, 'content!'), api_key: temp_credentials.dig('AccessKeyId', 0, 'content!'), secret_key: temp_credentials.dig('SecretAccessKey', 0, 'content!'), expiration: temp_credentials.dig('Expiration', 0, 'content!') } end def create_signature(credentials:, service:, host:, region:, path: '/', method: 'GET', params: {}, headers: {}, payload: '') url = URI::HTTPS.build(host: host, path: path, query: params.presence.to_param&.gsub('+', '%20')).to_s signer_options = { service: service, region: region, access_key_id: amazon_role_client_key(credentials), secret_access_key: amazon_role_client_secret(credentials), session_token: credentials[:session_token] } apply_service_specific_options(service, headers, signer_options, payload) signer = Aws::Sigv4::Signer.new(signer_options) signature = signer.sign_request(http_method: method, url: url, headers: headers, body: payload) headers_with_sig = merge_headers_with_sig_headers(headers, signature.headers) headers_with_sig = headers_with_sig.transform_keys { |key| key.gsub(/\b[a-z]/, &:upcase) } [url, headers_with_sig] end def apply_service_specific_options(service, headers, signer_options, payload) accept_headers = headers.key?('Accept') || headers.key?('accept') content_type = headers.key?('content-type') || headers.key?('Content-Type') case service when 'ec2' signer_options[:apply_checksum_header] = false headers.except!('Accept', 'Content-Type') when 's3' signer_options[:uri_escape_path] = false headers['Accept'] = 'application/xml' unless accept_headers headers['Content-Type'] = WWW_FORM_CONTENT_TYPE unless content_type headers['X-Amz-Content-SHA256'] = 'UNSIGNED-PAYLOAD' if payload.blank? when 'monitoring' signer_options[:apply_checksum_header] = false headers['Accept'] = 'application/json' unless accept_headers headers['Content-Type'] = WWW_FORM_CONTENT_TYPE unless content_type when 'lambda' signer_options[:apply_checksum_header] = false headers['Content-Type'] = WWW_FORM_CONTENT_TYPE unless content_type end end def merge_headers_with_sig_headers(headers, sig_headers) headers_keys = headers.transform_keys { |key| key.to_s.downcase } sig_headers_to_merge = sig_headers.reject { |key| headers_keys.include?(key.downcase) } headers.merge(sig_headers_to_merge) end def amazon_role_client_key(settings) settings[:access_key_id] || @connection.settings[:access_key_id] || AMAZON_ROLE_CLIENT_KEY end def amazon_role_client_secret(settings) settings[:secret_access_key] || @connection.settings[:access_key_id] || AMAZON_ROLE_CLIENT_SECRET end end private_constant :Private end end end end end