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.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.rubocop.yml +33 -0
- data/.ruby-version +1 -0
- data/.travis.yml +56 -0
- data/CHANGELOG.md +154 -0
- data/CONTRIBUTING.md +40 -0
- data/Gemfile +12 -0
- data/LICENSE +19 -0
- data/README.md +261 -0
- data/Rakefile +15 -0
- data/TESTING.md +70 -0
- data/lib/vagrant-s3auth.rb +14 -0
- data/lib/vagrant-s3auth/errors.rb +27 -0
- data/lib/vagrant-s3auth/extension/downloader.rb +84 -0
- data/lib/vagrant-s3auth/middleware/expand_s3_urls.rb +28 -0
- data/lib/vagrant-s3auth/plugin.rb +27 -0
- data/lib/vagrant-s3auth/util.rb +83 -0
- data/lib/vagrant-s3auth/version.rb +5 -0
- data/locales/en.yml +53 -0
- data/test/box/minimal +13 -0
- data/test/box/minimal.box +0 -0
- data/test/box/public-minimal +13 -0
- data/test/box/public-minimal.box +1 -0
- data/test/cleanup.rb +23 -0
- data/test/run.bats +147 -0
- data/test/setup.rb +34 -0
- data/test/support.rb +82 -0
- data/vagrant-s3auth.gemspec +25 -0
- metadata +157 -0
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
|
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
|
+
}
|