From a9ff3e3a9bdcba91b6bbac651a093f95d43b4d4e Mon Sep 17 00:00:00 2001
From: Matt Gresko <mgresko@veracode.com>
Date: Wed, 31 May 2017 17:23:18 -0400
Subject: [PATCH] AWS credentials chain

---
 Gemfile.lock                                  |  14 +--
 .../unreleased-ee/aws_credentials_chain.yml   |   4 +
 doc/integration/elasticsearch.md              |   3 +-
 lib/gitlab/elastic/client.rb                  |  15 ++-
 spec/lib/gitlab/elastic/client_spec.rb        | 100 ++++++++++++++++--
 5 files changed, 119 insertions(+), 17 deletions(-)
 create mode 100644 changelogs/unreleased-ee/aws_credentials_chain.yml

diff --git a/Gemfile.lock b/Gemfile.lock
index 0e94e279a6c4c..ea427a585912b 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -67,13 +67,13 @@ GEM
       execjs
       json
     awesome_print (1.2.0)
-    aws-sdk (2.7.8)
-      aws-sdk-resources (= 2.7.8)
-    aws-sdk-core (2.7.8)
+    aws-sdk (2.9.32)
+      aws-sdk-resources (= 2.9.32)
+    aws-sdk-core (2.9.32)
       aws-sigv4 (~> 1.0)
       jmespath (~> 1.0)
-    aws-sdk-resources (2.7.8)
-      aws-sdk-core (= 2.7.8)
+    aws-sdk-resources (2.9.32)
+      aws-sdk-core (= 2.9.32)
     aws-sigv4 (1.0.0)
     axiom-types (0.1.1)
       descendants_tracker (~> 0.0.4)
@@ -216,8 +216,8 @@ GEM
       multipart-post (>= 1.2, < 3)
     faraday_middleware (0.11.0.1)
       faraday (>= 0.7.4, < 1.0)
-    faraday_middleware-aws-signers-v4 (0.1.5)
-      aws-sdk (~> 2.1)
+    faraday_middleware-aws-signers-v4 (0.1.7)
+      aws-sdk-resources (~> 2)
       faraday (~> 0.9)
     faraday_middleware-multi_json (0.0.6)
       faraday_middleware
diff --git a/changelogs/unreleased-ee/aws_credentials_chain.yml b/changelogs/unreleased-ee/aws_credentials_chain.yml
new file mode 100644
index 0000000000000..f8181c7fe9a10
--- /dev/null
+++ b/changelogs/unreleased-ee/aws_credentials_chain.yml
@@ -0,0 +1,4 @@
+---
+title: Adding support for AWS ec2 instance profile credentials with elasticsearch
+merge_request:
+author: Matt Gresko
diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md
index e381f3f18260f..b7688f30adff7 100644
--- a/doc/integration/elasticsearch.md
+++ b/doc/integration/elasticsearch.md
@@ -58,7 +58,7 @@ The following Elasticsearch settings are available:
 | `Use experimental repository indexer` | Perform repository indexing using [GitLab Elasticsearch Indexer](https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer). |
 | `Search with Elasticsearch enabled` | Enables/disables using Elasticsearch in search. |
 | `URL`                              | The URL to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., "http://host1, https://host2:9200"). |
-| `Using AWS hosted Elasticsearch with IAM credentials` | Sign your Elasticsearch requests using [AWS IAM authorization][aws-iam]. The access key must be allowed to perform `es:*` actions. |
+| `Using AWS hosted Elasticsearch with IAM credentials` | Sign your Elasticsearch requests using [AWS IAM authorization][aws-iam] or [AWS EC2 Instance Profile Credentials][aws-instance-profile]. The policies must be configured to allow `es:*` actions. |
 | `AWS Region` | The AWS region your Elasticsearch service is located in. |
 | `AWS Access Key` | The AWS access key. |
 | `AWS Secret Access Key` | The AWS secret access key. |
@@ -325,6 +325,7 @@ Make sure you indexed all the database data as stated above (`sudo gitlab-rake g
 [ee-1305]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1305
 [aws-elasticsearch]: http://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-gsg.html
 [aws-iam]: http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html
+[aws-instance-profile]: http://docs.aws.amazon.com/codedeploy/latest/userguide/getting-started-create-iam-instance-profile.html#getting-started-create-iam-instance-profile-cli
 [ee-109]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/109 "Elasticsearch Merge Request"
 [elasticsearch]: https://www.elastic.co/products/elasticsearch "Elasticsearch website"
 [install]: https://www.elastic.co/guide/en/elasticsearch/reference/current/_installation.html "Elasticsearch installation documentation"
diff --git a/lib/gitlab/elastic/client.rb b/lib/gitlab/elastic/client.rb
index f51672361d918..d3eb2735c78b9 100644
--- a/lib/gitlab/elastic/client.rb
+++ b/lib/gitlab/elastic/client.rb
@@ -13,7 +13,7 @@ def self.build(config)
         base_config = { urls: config[:url] }
 
         if config[:aws]
-          creds = Aws::Credentials.new(config[:aws_access_key], config[:aws_secret_access_key])
+          creds = resolve_aws_credentials(config)
           region = config[:aws_region]
 
           ::Elasticsearch::Client.new(base_config) do |fmid|
@@ -23,6 +23,19 @@ def self.build(config)
           ::Elasticsearch::Client.new(base_config)
         end
       end
+
+      def self.resolve_aws_credentials(config)
+        # Resolve credentials in order
+        # 1.  Static config
+        # 2.  ec2 instance profile
+        credentials = [
+          Aws::Credentials.new(config[:aws_access_key], config[:aws_secret_access_key]),
+          Aws::InstanceProfileCredentials.new
+        ]
+        credentials.find do |creds|
+          creds&.set?
+        end
+      end
     end
   end
 end
diff --git a/spec/lib/gitlab/elastic/client_spec.rb b/spec/lib/gitlab/elastic/client_spec.rb
index 6b3be78661851..dd609d0a720d9 100644
--- a/spec/lib/gitlab/elastic/client_spec.rb
+++ b/spec/lib/gitlab/elastic/client_spec.rb
@@ -1,6 +1,33 @@
 require 'spec_helper'
 
 describe Gitlab::Elastic::Client do
+  let(:creds_valid_response) do
+    '{
+      "Code": "Success",
+      "Type": "AWS-HMAC",
+      "AccessKeyId": "0",
+      "SecretAccessKey": "0",
+      "Token": "token",
+      "Expiration": "2018-12-16T01:51:37Z",
+      "LastUpdated": "2009-11-23T0:00:00Z"
+    }'
+  end
+
+  let(:creds_fail_response) do
+    '{
+      "Code": "ErrorCode",
+      "Message": "ErrorMsg",
+      "LastUpdated": "2009-11-23T0:00:00Z"
+    }'
+  end
+
+  def stub_instance_credentials(creds_response)
+    stub_request(:get, "http://169.254.169.254/latest/meta-data/iam/security-credentials/")
+      .to_return(status: 200, body: "RoleName", headers: {})
+    stub_request(:get, "http://169.254.169.254/latest/meta-data/iam/security-credentials/RoleName")
+      .to_return(status: 200, body: creds_response, headers: {})
+  end
+
   describe 'build' do
     let(:client) { described_class.build(params) }
 
@@ -16,7 +43,7 @@
       end
     end
 
-    context 'with AWS IAM credentials' do
+    context 'with AWS IAM static credentials' do
       let(:params) do
         {
           url: 'http://example-elastic:9200',
@@ -27,21 +54,78 @@
         }
       end
 
-      it 'signs requests' do
+      it 'signs_requests' do
+        stub_instance_credentials(creds_fail_response)
         travel_to(Time.parse('20170303T133952Z')) do
           stub_request(:get, 'http://example-elastic:9200/foo/_all/1')
             .with(
               headers: {
-               'Authorization'        => 'AWS4-HMAC-SHA256 Credential=0/20170303/us-east-1/es/aws4_request, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date, Signature=4ba2aae19a476152dacf5a2191da67b0cf81b9d7152dab5c42b1bba701da19f1',
-               'Content-Type'         => 'application/json',
-               'X-Amz-Content-Sha256' => 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
-               'X-Amz-Date'           => '20170303T133952Z'
-            })
-            .to_return(status: 200, body: [:fake_response])
+                'Authorization'        => 'AWS4-HMAC-SHA256 Credential=0/20170303/us-east-1/es/aws4_request, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date, Signature=4ba2aae19a476152dacf5a2191da67b0cf81b9d7152dab5c42b1bba701da19f1',
+                'Content-Type'         => 'application/json',
+                'X-Amz-Content-Sha256' => 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
+                'X-Amz-Date'           => '20170303T133952Z'
+              })
+              .to_return(status: 200, body: [:fake_response])
 
           expect(client.get(index: 'foo', id: 1)).to eq([:fake_response])
         end
       end
     end
   end
+
+  describe 'resolve_aws_credentials' do
+    let(:creds) { described_class.resolve_aws_credentials(params) }
+
+    context 'with AWS IAM static credentials' do
+      let(:params) do
+        {
+          url: 'http://example-elastic:9200',
+          aws: true,
+          aws_region: 'us-east-1',
+          aws_access_key: '0',
+          aws_secret_access_key: '0'
+        }
+      end
+
+      it 'returns credentials from static credentials' do
+        stub_instance_credentials(creds_fail_response)
+
+        expect(creds.credentials.access_key_id).to eq '0'
+        expect(creds.credentials.secret_access_key).to eq '0'
+      end
+    end
+
+    context 'with AWS ec2 instance profile' do
+      let(:params) do
+        {
+          url: 'http://example-elastic:9200',
+          aws: true,
+          aws_region: 'us-east-1'
+        }
+      end
+
+      it 'returns credentials from ec2 instance profile' do
+        stub_instance_credentials(creds_valid_response)
+
+        expect(creds.credentials.access_key_id).to eq '0'
+        expect(creds.credentials.secret_access_key).to eq '0'
+      end
+    end
+
+    context 'with AWS no credentials' do
+      let(:params) do
+        {
+          url: 'http://example-elastic:9200',
+          aws: true,
+          aws_region: 'us-east-1'
+        }
+      end
+
+      it 'returns nil' do
+        stub_instance_credentials(creds_fail_response)
+
+        expect(creds).to be_nil
+      end
+    end
+  end
 end
-- 
GitLab