vagrant-s3auth-mfa 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
}
|