vagrant-s3auth-mfa 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ }