vagrant-s3auth-mfa 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'rubocop/rake_task'
4
+
5
+ Dir.chdir(File.expand_path('../', __FILE__))
6
+
7
+ Bundler::GemHelper.install_tasks
8
+
9
+ RuboCop::RakeTask.new(:lint)
10
+
11
+ task :test do
12
+ sh 'bats test/run.bats'
13
+ end
14
+
15
+ task default: %w[lint test]
data/TESTING.md ADDED
@@ -0,0 +1,70 @@
1
+ # Testing
2
+
3
+ No unit testing, since the project is so small. But a full suite of acceptance
4
+ tests that run using [Bats: Bash Automated Testing System][bats]! Basically, the
5
+ acceptance tests run `vagrant box add S3_URL` with a bunch of S3 URLs and box
6
+ types, and assert that everything works!
7
+
8
+ See [the .travis.yml CI configuration](.travis.yml) for a working example.
9
+
10
+ ## Environment variables
11
+
12
+ You'll need to export the below. Recommended values included when not sensitive.
13
+
14
+ ```bash
15
+ # AWS credentials with permissions to create S3 buckets
16
+ export AWS_ACCESS_KEY_ID=
17
+ export AWS_SECRET_ACCESS_KEY=
18
+
19
+ # Atlas (Vagrant Cloud) API credentials
20
+ export ATLAS_USERNAME="vagrant-s3auth"
21
+ export ATLAS_TOKEN
22
+
23
+ # Base name of bucket. Must be unique.
24
+ export VAGRANT_S3AUTH_BUCKET="testing.vagrant-s3auth.com"
25
+
26
+ # If specified as 'metadata', will upload 'box/metadata' and 'box/metadata.box'
27
+ # to each S3 bucket
28
+ export VAGRANT_S3AUTH_BOX_BASE="minimal"
29
+
30
+ # Base name of Atlas (Vagrant Cloud) box. Atlas boxes can never re-use a once
31
+ # existing name, so include a timestamp or random string in the name.
32
+ export VAGRANT_S3AUTH_ATLAS_BOX_NAME="vagrant-s3auth-192458"
33
+
34
+ # Additional S3 region to use in testing. US Standard is always used.
35
+ export VAGRANT_S3AUTH_REGION_NONSTANDARD="eu-west-1"
36
+ ```
37
+
38
+ [bats]: https://github.com/sstephenson/bats
39
+
40
+ ## Running tests
41
+
42
+ You'll need [Bats][bats] installed! Then:
43
+
44
+ ```bash
45
+ # export env vars as described
46
+ $ test/setup.rb
47
+ $ rake test
48
+ # hack hack hack
49
+ $ rake test
50
+ $ test/cleanup.rb
51
+ ```
52
+
53
+ ## Scripts
54
+
55
+ ### test/setup.rb
56
+
57
+ Creates two S3 buckets—one in US Standard (`us-east-1`) and one in
58
+ `$VAGRANT_S3AUTH_REGION_NONSTANDARD`, both with the contents of the box
59
+ directory.
60
+
61
+ Then creates an Atlas (Vagrant Cloud) box with one version with one VirtualBox
62
+ provider that points to one of the S3 boxes at random.
63
+
64
+ ### test/cleanup.rb
65
+
66
+ Destroys S3 buckets and Atlas box.
67
+
68
+ ## run.bats
69
+
70
+ Attempts to `vagrant box add` the boxes on S3 in every way possible.
@@ -0,0 +1,14 @@
1
+ require 'pathname'
2
+
3
+ require 'vagrant-s3auth/plugin'
4
+
5
+ module VagrantPlugins
6
+ module S3Auth
7
+ def self.source_root
8
+ @source_root ||= Pathname.new(File.expand_path('../../', __FILE__))
9
+ end
10
+
11
+ I18n.load_path << File.expand_path('locales/en.yml', source_root)
12
+ I18n.reload!
13
+ end
14
+ end
@@ -0,0 +1,27 @@
1
+ require 'vagrant'
2
+
3
+ module VagrantPlugins
4
+ module S3Auth
5
+ module Errors
6
+ class VagrantS3AuthError < Vagrant::Errors::VagrantError
7
+ error_namespace('vagrant_s3auth.errors')
8
+ end
9
+
10
+ class MissingCredentialsError < VagrantS3AuthError
11
+ error_key(:missing_credentials)
12
+ end
13
+
14
+ class MalformedShorthandURLError < VagrantS3AuthError
15
+ error_key(:malformed_shorthand_url)
16
+ end
17
+
18
+ class BucketLocationAccessDeniedError < VagrantS3AuthError
19
+ error_key(:bucket_location_access_denied_error)
20
+ end
21
+
22
+ class S3APIError < VagrantS3AuthError
23
+ error_key(:s3_api_error)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,84 @@
1
+ require 'uri'
2
+
3
+ require 'vagrant/util/downloader'
4
+ require 'vagrant-s3auth/util'
5
+
6
+ S3Auth = VagrantPlugins::S3Auth
7
+
8
+ module Vagrant
9
+ module Util
10
+ class Downloader
11
+ def s3auth_credential_source
12
+ credential_provider = S3Auth::Util.s3_credential_provider
13
+ case credential_provider
14
+ when ::Aws::Credentials
15
+ I18n.t(
16
+ 'vagrant_s3auth.downloader.env_credential_provider',
17
+ access_key: credential_provider.credentials.access_key_id,
18
+ env_var: S3Auth::Util::AWS_ACCESS_KEY_ENV_VARS.find { |k| ENV.key?(k) }
19
+ )
20
+ when ::Aws::SharedCredentials
21
+ I18n.t(
22
+ 'vagrant_s3auth.downloader.profile_credential_provider',
23
+ access_key: credential_provider.credentials.access_key_id,
24
+ profile: credential_provider.profile_name
25
+ )
26
+ end
27
+ end
28
+
29
+ def s3auth_download(options, subprocess_options, &data_proc)
30
+ # The URL sent to curl is always the last argument. We have to rely
31
+ # on this implementation detail because we need to hook into both
32
+ # HEAD and GET requests.
33
+ url = options.last
34
+
35
+ s3_object = S3Auth::Util.s3_object_for(url)
36
+ return unless s3_object
37
+
38
+ @logger.info("s3auth: Discovered S3 URL: #{@source}")
39
+ @logger.debug("s3auth: Bucket: #{s3_object.bucket.name.inspect}")
40
+ @logger.debug("s3auth: Key: #{s3_object.key.inspect}")
41
+
42
+ method = options.any? { |o| o == '-I' } ? :head : :get
43
+
44
+ @logger.info("s3auth: Generating signed URL for #{method.upcase}")
45
+
46
+ @ui.detail(s3auth_credential_source) if @ui
47
+
48
+ url.replace(S3Auth::Util.s3_url_for(method, s3_object).to_s)
49
+
50
+ execute_curl_without_s3auth(options, subprocess_options, &data_proc)
51
+ rescue Errors::DownloaderError => e
52
+ if e.message =~ /403 Forbidden/
53
+ e.message << "\n\n"
54
+ e.message << I18n.t('vagrant_s3auth.errors.box_download_forbidden',
55
+ bucket: s3_object && s3_object.bucket.name)
56
+ end
57
+ raise
58
+ rescue ::Aws::Errors::MissingCredentialsError
59
+ raise S3Auth::Errors::MissingCredentialsError
60
+ rescue ::Aws::Errors::ServiceError => e
61
+ raise S3Auth::Errors::S3APIError, error: e
62
+ rescue ::Seahorse::Client::NetworkingError => e
63
+ # Vagrant ignores download errors during e.g. box update checks
64
+ # because an internet connection isn't necessary if the box is
65
+ # already downloaded. Vagrant isn't expecting AWS's
66
+ # Seahorse::Client::NetworkingError, so we cast it to the
67
+ # DownloaderError Vagrant expects.
68
+ raise Errors::DownloaderError, message: e
69
+ end
70
+
71
+ def execute_curl_with_s3auth(options, subprocess_options, &data_proc)
72
+ execute_curl_without_s3auth(options, subprocess_options, &data_proc)
73
+ rescue Errors::DownloaderError => e
74
+ # Ensure the progress bar from the just-failed request is cleared.
75
+ @ui.clear_line if @ui
76
+
77
+ s3auth_download(options, subprocess_options, &data_proc) || (raise e)
78
+ end
79
+
80
+ alias execute_curl_without_s3auth execute_curl
81
+ alias execute_curl execute_curl_with_s3auth
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,28 @@
1
+ require 'uri'
2
+
3
+ module VagrantPlugins
4
+ module S3Auth
5
+ class ExpandS3Urls
6
+ def initialize(app, _)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ env[:box_urls].map! do |url_string|
12
+ url = URI(url_string)
13
+
14
+ if url.scheme == 's3'
15
+ bucket = url.host
16
+ key = url.path[1..-1]
17
+ raise Errors::MalformedShorthandURLError, url: url unless bucket && key
18
+ next "http://s3-placeholder.amazonaws.com/#{bucket}/#{key}"
19
+ end
20
+
21
+ url_string
22
+ end
23
+
24
+ @app.call(env)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,27 @@
1
+ begin
2
+ require 'vagrant'
3
+ rescue LoadError
4
+ raise 'The Vagrant S3Auth plugin must be run within Vagrant.'
5
+ end
6
+
7
+ require_relative 'errors'
8
+ require_relative 'extension/downloader'
9
+
10
+ module VagrantPlugins
11
+ module S3Auth
12
+ class Plugin < Vagrant.plugin('2')
13
+ Vagrant.require_version('>= 1.5.1')
14
+
15
+ name 's3auth'
16
+
17
+ description <<-DESC
18
+ Use versioned Vagrant boxes with S3 authentication.
19
+ DESC
20
+
21
+ action_hook(:s3_urls, :authenticate_box_url) do |hook|
22
+ require_relative 'middleware/expand_s3_urls'
23
+ hook.prepend(ExpandS3Urls)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,83 @@
1
+ require 'aws-sdk'
2
+ require 'log4r'
3
+ require 'net/http'
4
+ require 'uri'
5
+
6
+ module VagrantPlugins
7
+ module S3Auth
8
+ module Util
9
+ S3_HOST_MATCHER = /^((?<bucket>[[:alnum:]\-\.]+).)?s3([[:alnum:]\-\.]+)?\.amazonaws\.com$/
10
+
11
+ # The list of environment variables that the AWS Ruby SDK searches
12
+ # for access keys. Sadly, there's no better way to determine which
13
+ # environment variable the Ruby SDK is using without mirroring the
14
+ # logic ourself.
15
+ #
16
+ # See: https://github.com/aws/aws-sdk-ruby/blob/ab0eb18d0ce0a515254e207dae772864c34b048d/aws-sdk-core/lib/aws-sdk-core/credential_provider_chain.rb#L42
17
+ AWS_ACCESS_KEY_ENV_VARS = %w[AWS_ACCESS_KEY_ID AMAZON_ACCESS_KEY_ID AWS_ACCESS_KEY].freeze
18
+
19
+ DEFAULT_REGION = 'us-east-1'.freeze
20
+
21
+ LOCATION_TO_REGION = Hash.new { |_, key| key }.merge(
22
+ '' => DEFAULT_REGION,
23
+ 'EU' => 'eu-west-1'
24
+ )
25
+
26
+ class NullObject
27
+ def method_missing(*) # rubocop:disable Style/MethodMissing
28
+ nil
29
+ end
30
+ end
31
+
32
+ def self.s3_client(region = DEFAULT_REGION)
33
+ ::Aws::S3::Client.new(region: region)
34
+ end
35
+
36
+ def self.s3_resource(region = DEFAULT_REGION)
37
+ ::Aws::S3::Resource.new(client: s3_client(region))
38
+ end
39
+
40
+ def self.s3_object_for(url, follow_redirect = true)
41
+ url = URI(url)
42
+
43
+ if url.scheme == 's3'
44
+ bucket = url.host
45
+ key = url.path[1..-1]
46
+ raise Errors::MalformedShorthandURLError, url: url unless bucket && key
47
+ elsif match = S3_HOST_MATCHER.match(url.host)
48
+ components = url.path.split('/').delete_if(&:empty?)
49
+ bucket = match['bucket'] || components.shift
50
+ key = components.join('/')
51
+ end
52
+
53
+ if bucket && key
54
+ s3_resource(get_bucket_region(bucket)).bucket(bucket).object(key)
55
+ elsif follow_redirect
56
+ response = Net::HTTP.get_response(url) rescue nil
57
+ if response.is_a?(Net::HTTPRedirection)
58
+ s3_object_for(response['location'], false)
59
+ end
60
+ end
61
+ end
62
+
63
+ def self.s3_url_for(method, s3_object)
64
+ s3_object.presigned_url(method, expires_in: 60 * 10)
65
+ end
66
+
67
+ def self.get_bucket_region(bucket)
68
+ LOCATION_TO_REGION[
69
+ s3_client.get_bucket_location(bucket: bucket).location_constraint
70
+ ]
71
+ rescue ::Aws::S3::Errors::AccessDenied
72
+ raise Errors::BucketLocationAccessDeniedError, bucket: bucket
73
+ end
74
+
75
+ def self.s3_credential_provider
76
+ # Providing a NullObject here is the same as instantiating a
77
+ # client without specifying a credentials config, like we do in
78
+ # `self.s3_client`.
79
+ ::Aws::CredentialProviderChain.new(NullObject.new).resolve
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,5 @@
1
+ module VagrantPlugins
2
+ module S3Auth
3
+ VERSION = '1.4.0'.freeze
4
+ end
5
+ end
data/locales/en.yml ADDED
@@ -0,0 +1,53 @@
1
+ en:
2
+ vagrant_s3auth:
3
+ downloader:
4
+ env_credential_provider: |-
5
+ Signing S3 request with key '%{access_key}' loaded from $%{env_var}
6
+
7
+ profile_credential_provider: |-
8
+ Signing S3 request with key '%{access_key}' loaded from profile '%{profile}'
9
+
10
+ errors:
11
+ missing_credentials: |-
12
+ Unable to find AWS credentials.
13
+
14
+ Ensure the following variables are set in your environment, or set
15
+ them at the top of your Vagrantfile:
16
+
17
+ AWS_ACCESS_KEY_ID
18
+ AWS_SECRET_ACCESS_KEY
19
+
20
+ Alternatively, you can create a credential profile and set the
21
+
22
+ AWS_PROFILE
23
+
24
+ environment variable. Consult the documentation for details.
25
+
26
+ malformed_shorthand_url: |-
27
+ Malformed shorthand S3 box URL:
28
+
29
+ %{url}
30
+
31
+ Check your `box_url` setting.
32
+
33
+ s3_api_error: |-
34
+ Unable to communicate with Amazon S3 to download box. The S3 API reports:
35
+
36
+ %{error}
37
+
38
+ bucket_location_access_denied_error: |-
39
+ Request for box's Amazon S3 region was denied.
40
+
41
+ This usually indicates that your user account is misconfigured. Ensure
42
+ your IAM policy allows the "s3:GetBucketLocation" action for your bucket:
43
+
44
+ arn:aws:s3:::%{bucket}
45
+
46
+ box_download_forbidden: |-
47
+ This box is hosted on Amazon S3. A 403 Forbidden error usually indicates
48
+ that your user account is misconfigured. Ensure your IAM policy allows
49
+ the "s3:GetObject" action for your bucket:
50
+
51
+ arn:aws:s3:::%{bucket}/*
52
+
53
+ It may also indicate the box does not exist, so check your spelling.
data/test/box/minimal ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "vagrant-s3auth/minimal",
3
+ "description": "This box contains company secrets.",
4
+ "versions": [{
5
+ "version": "1.0.1",
6
+ "providers": [{
7
+ "name": "virtualbox",
8
+ "url": "%{box_url}",
9
+ "checksum_type": "sha1",
10
+ "checksum": "8ea536dd3092cf159f02405edd44ded5b62ba4e6"
11
+ }]
12
+ }]
13
+ }
Binary file
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "vagrant-s3auth/public-minimal",
3
+ "description": "This box contains no company secrets.",
4
+ "versions": [{
5
+ "version": "1.0.1",
6
+ "providers": [{
7
+ "name": "virtualbox",
8
+ "url": "%{box_url}",
9
+ "checksum_type": "sha1",
10
+ "checksum": "8ea536dd3092cf159f02405edd44ded5b62ba4e6"
11
+ }]
12
+ }]
13
+ }