vagrant_cloud 3.1.1 → 3.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 06ac337d41c05fa0ab3f4dba19eb4b48369b7b21e87adea9beaf22c24d7a06ec
4
- data.tar.gz: 20c9e7e4c6c6a045a34a5509e05e626a159069d76993d3f70bdf2f333a219083
3
+ metadata.gz: 013f62b5aeeb2125349790b206a2fc42ee6e60288bb28fab28aefa129af8c55c
4
+ data.tar.gz: a928446b785eb485285ba3e030589391cc3fd3e49fdeb15053677e58ff864e2e
5
5
  SHA512:
6
- metadata.gz: 0c4fd65d40092ff9161d5330afda571efc2d852b23aedf7f8327d803eb2e096792999ebb3f69dd3cbce4663d5c6b79db121531737258084e18c634f626d25d35
7
- data.tar.gz: 69eb59e4a61257533baf2707b998b29a6f88f8b37a4c54f6dea8daca3f19e0c93d479d68ae42a277f1ee3c972622625b380fbd7f5db9bfd8e212a583fa706118
6
+ metadata.gz: 58ae036121f99b0e1054e49deb30644510f1310ab55ea7f07d2be89b6b27287f4c5a933ff20199e1eb64556506d548677beb0e543d6a7bf5f8e3217698c12c99
7
+ data.tar.gz: 3de0d70c11834c2228c78201600f07354a788b563d378e222001c9fbc1651f40e4ae1c0d3346fd342ccad16cb48d8a5efd6471987ab5321d1dd71ed0644d9312
data/README.md CHANGED
@@ -13,6 +13,14 @@ The Vagrant Cloud library provides two methods for interacting with the Vagrant
13
13
  first is direct interaction using a `VagrantCloud::Client` instance. The second is a basic
14
14
  model based approach using a `VagrantCloud::Account` instance.
15
15
 
16
+ ### Authentication
17
+
18
+ The access token that is used for authenticated requests can be set in one of three ways:
19
+
20
+ * Static access token set directly in the client
21
+ * Static access token extracted from the `VAGRANT_CLOUD_TOKEN` environment variable
22
+ * Generated [HCP service principal](https://developer.hashicorp.com/hcp/docs/hcp/iam/service-principal) access token when `HCP_CLIENT_ID` and `HCP_CLIENT_SECRET` environment variables are set
23
+
16
24
  ### Direct Client
17
25
 
18
26
  The `VagrantCloud::Client` class contains all the underlying functionality which with
@@ -0,0 +1,140 @@
1
+ require "oauth2"
2
+
3
+ module VagrantCloud
4
+ class Auth
5
+
6
+ # Default authentication URL
7
+ DEFAULT_AUTH_URL = "https://auth.idp.hashicorp.com".freeze
8
+ # Default authorize path
9
+ DEFAULT_AUTH_PATH = "/oauth2/auth".freeze
10
+ # Default token path
11
+ DEFAULT_TOKEN_PATH = "/oauth2/token".freeze
12
+ # Number of seconds to pad token expiry
13
+ TOKEN_EXPIRY_PADDING = 5
14
+
15
+ # HCP configuration for generating authentication tokens
16
+ #
17
+ # @param [String] client_id Service principal client ID
18
+ # @param [String] client_secret Service principal client secret
19
+ # @param [String] auth_url Authentication URL end point
20
+ # @param [String] auth_path Authorization path (relative to end point)
21
+ # @param [String] token_path Token path (relative to end point)
22
+ HCPConfig = Struct.new(:client_id, :client_secret, :auth_url, :auth_path, :token_path, keyword_init: true) do
23
+ # Raise exception if any values are missing
24
+ def validate!
25
+ [:client_id, :client_secret, :auth_url, :auth_path, :token_path].each do |name|
26
+ raise ArgumentError,
27
+ "Missing required HCP authentication configuration value: HCP_#{name.to_s.upcase}" if self.send(name).to_s.empty?
28
+ end
29
+ end
30
+ end
31
+
32
+ # HCP token
33
+ #
34
+ # @param [String] token HCP token value
35
+ # @param [Integer] expires_at Epoch seconds
36
+ HCPToken = Struct.new(:token, :expires_at, keyword_init: true) do
37
+ # Raise exception if any values are missing
38
+ def validate!
39
+ [:token, :expires_at].each do |name|
40
+ raise ArgumentError,
41
+ "Missing required token value - #{name.inspect}" if self.send(name).nil?
42
+ end
43
+ end
44
+
45
+ # @return [Boolean] token is expired
46
+ # @note Will show token as expired TOKEN_EXPIRY_PADDING
47
+ # seconds prior to actual expiry
48
+ def expired?
49
+ validate!
50
+
51
+ Time.now.to_i > (expires_at - TOKEN_EXPIRY_PADDING)
52
+ end
53
+
54
+ # @return [Boolean] token is not expired
55
+ def valid?
56
+ !expired?
57
+ end
58
+ end
59
+
60
+ # Create a new auth instance
61
+ #
62
+ # @param [String] access_token Static access token
63
+ # @note If no access token is provided, the token will be extracted
64
+ # from the VAGRANT_CLOUD_TOKEN environment variable. If that value
65
+ # is not set, the HCP_CLIENT_ID and HCP_CLIENT_SECRET environment
66
+ # variables will be checked. If found, tokens will be generated as
67
+ # needed using the client id and secret. Otherwise, no token will
68
+ # will be available.
69
+ def initialize(access_token: nil)
70
+ @token = access_token
71
+
72
+ # The Vagrant Cloud token has precedence over
73
+ # anything else, so if it is set then it is
74
+ # the only value used.
75
+ @token = ENV["VAGRANT_CLOUD_TOKEN"] if @token.nil?
76
+
77
+ # If there is no token set, attempt to load HCP configuration
78
+ if @token.to_s.empty? && (ENV["HCP_CLIENT_ID"] || ENV["HCP_CLIENT_SECRET"])
79
+ @config = HCPConfig.new(
80
+ client_id: ENV["HCP_CLIENT_ID"],
81
+ client_secret: ENV["HCP_CLIENT_SECRET"],
82
+ auth_url: ENV.fetch("HCP_AUTH_URL", DEFAULT_AUTH_URL),
83
+ auth_path: ENV.fetch("HCP_AUTH_PATH", DEFAULT_AUTH_PATH),
84
+ token_path: ENV.fetch("HCP_TOKEN_PATH", DEFAULT_TOKEN_PATH)
85
+ )
86
+
87
+ # Validate configuration is populated
88
+ @config.validate!
89
+ end
90
+ end
91
+
92
+ # @return [String] authentication token
93
+ def token
94
+ # If a static token is defined, use that value
95
+ return @token if @token
96
+
97
+ # If no configuration is set, there is no auth to provide
98
+ return if @config.nil?
99
+
100
+ # If an HCP token exists and is not expired
101
+ return @hcp_token.token if @hcp_token&.valid?
102
+
103
+ # Generate a new HCP token
104
+ refresh_token!
105
+
106
+ @hcp_token.token
107
+ end
108
+
109
+ # @return [Boolean] Authentication token is available
110
+ def available?
111
+ !!(@token || @config)
112
+ end
113
+
114
+ private
115
+
116
+ # Refresh the HCP oauth2 token.
117
+ # @todo rescue exceptions and make them nicer
118
+ def refresh_token!
119
+ client = OAuth2::Client.new(
120
+ @config.client_id,
121
+ @config.client_secret,
122
+ site: @config.auth_url,
123
+ authorize_url: @config.auth_path,
124
+ token_url: @config.token_path,
125
+ )
126
+
127
+ begin
128
+ response = client.client_credentials.get_token
129
+ @hcp_token = HCPToken.new(
130
+ token: response.token,
131
+ expires_at: response.expires_at,
132
+ )
133
+ rescue OAuth2::Error => err
134
+ raise Error::AuthenticationError,
135
+ err.response.body.chomp,
136
+ err.response.status
137
+ end
138
+ end
139
+ end
140
+ end
@@ -23,8 +23,6 @@ module VagrantCloud
23
23
  DEFAULT_INSTRUMENTOR
24
24
  end
25
25
 
26
- # @return [String] Access token for Vagrant Cloud
27
- attr_reader :access_token
28
26
  # @return [String] Base request path
29
27
  attr_reader :path_base
30
28
  # @return [String] URL for initializing connection
@@ -52,16 +50,12 @@ module VagrantCloud
52
50
  if @path_base.empty? || @path_base == API_V1_PATH || @path_base == API_V2_PATH
53
51
  @path_base = nil
54
52
  end
55
- @access_token = access_token.dup.freeze if access_token
56
- if !@access_token && ENV["VAGRANT_CLOUD_TOKEN"]
57
- @access_token = ENV["VAGRANT_CLOUD_TOKEN"].dup.freeze
58
- end
53
+ @auth = Auth.new(access_token: access_token)
59
54
  @retry_count = retry_count.nil? ? IDEMPOTENT_RETRIES : retry_count.to_i
60
55
  @retry_interval = retry_interval.nil? ? IDEMPOTENT_RETRY_INTERVAL : retry_interval.to_i
61
56
  @instrumentor = instrumentor.nil? ? Instrumentor::Collection.new : instrumentor
62
57
  headers = {}.tap do |h|
63
58
  h["Accept"] = "application/json"
64
- h["Authorization"] = "Bearer #{@access_token}" if @access_token
65
59
  h["Content-Type"] = "application/json"
66
60
  end
67
61
  @connection_lock = Mutex.new
@@ -71,6 +65,11 @@ module VagrantCloud
71
65
  )
72
66
  end
73
67
 
68
+ # @return [String] Access token for Vagrant Cloud
69
+ def access_token
70
+ @auth.token
71
+ end
72
+
74
73
  # Use the remote connection
75
74
  #
76
75
  # @param [Boolean] wait Wait for the connection to be available
@@ -79,16 +78,28 @@ module VagrantCloud
79
78
  def with_connection(wait: true)
80
79
  raise ArgumentError,
81
80
  "Block expected but no block given" if !block_given?
81
+
82
+ # Adds authentication header to connection if available
83
+ set_authentication = ->(conn) {
84
+ if @auth.available?
85
+ conn.connection[:headers]["Authorization"] = "Bearer #{@auth.token}"
86
+ end
87
+ }
88
+
82
89
  if !wait
83
90
  raise Error::ClientError::ConnectionLockedError,
84
91
  "Connection is currently locked" if !@connection_lock.try_lock
92
+ set_authentication.call(@connection)
85
93
  begin
86
94
  yield @connection
87
95
  ensure
88
96
  @connection_lock.unlock
89
97
  end
90
98
  else
91
- @connection_lock.synchronize { yield @connection }
99
+ @connection_lock.synchronize do
100
+ set_authentication.call(@connection)
101
+ yield @connection
102
+ end
92
103
  end
93
104
  end
94
105
 
@@ -29,6 +29,13 @@ module VagrantCloud
29
29
  end
30
30
 
31
31
  class ConnectionLockedError < ClientError; end
32
+ class AuthenticationError < ClientError
33
+ def initialize(msg, http_code)
34
+ @error_arr = [msg]
35
+ @error_code = http_code.to_i
36
+ super(msg)
37
+ end
38
+ end
32
39
  end
33
40
 
34
41
  class BoxError < Error
data/lib/vagrant_cloud.rb CHANGED
@@ -8,6 +8,7 @@ require "thread"
8
8
 
9
9
  module VagrantCloud
10
10
  autoload :Account, "vagrant_cloud/account"
11
+ autoload :Auth, "vagrant_cloud/auth"
11
12
  autoload :Box, "vagrant_cloud/box"
12
13
  autoload :Client, "vagrant_cloud/client"
13
14
  autoload :Data, "vagrant_cloud/data"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vagrant_cloud
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.1
4
+ version: 3.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - HashiCorp
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-01-18 00:00:00.000000000 Z
12
+ date: 2024-11-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: excon
@@ -17,42 +17,56 @@ dependencies:
17
17
  requirements:
18
18
  - - "~>"
19
19
  - !ruby/object:Gem::Version
20
- version: '0.73'
20
+ version: '1.0'
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - "~>"
26
26
  - !ruby/object:Gem::Version
27
- version: '0.73'
27
+ version: '1.0'
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: log4r
30
30
  requirement: !ruby/object:Gem::Requirement
31
31
  requirements:
32
32
  - - "~>"
33
33
  - !ruby/object:Gem::Version
34
- version: 1.1.10
34
+ version: '1.1'
35
35
  type: :runtime
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
39
  - - "~>"
40
40
  - !ruby/object:Gem::Version
41
- version: 1.1.10
41
+ version: '1.1'
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: rexml
44
44
  requirement: !ruby/object:Gem::Requirement
45
45
  requirements:
46
46
  - - "~>"
47
47
  - !ruby/object:Gem::Version
48
- version: 3.2.5
48
+ version: '3.3'
49
49
  type: :runtime
50
50
  prerelease: false
51
51
  version_requirements: !ruby/object:Gem::Requirement
52
52
  requirements:
53
53
  - - "~>"
54
54
  - !ruby/object:Gem::Version
55
- version: 3.2.5
55
+ version: '3.3'
56
+ - !ruby/object:Gem::Dependency
57
+ name: oauth2
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '2.0'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '2.0'
56
70
  - !ruby/object:Gem::Dependency
57
71
  name: rake
58
72
  requirement: !ruby/object:Gem::Requirement
@@ -105,6 +119,7 @@ files:
105
119
  - README.md
106
120
  - lib/vagrant_cloud.rb
107
121
  - lib/vagrant_cloud/account.rb
122
+ - lib/vagrant_cloud/auth.rb
108
123
  - lib/vagrant_cloud/box.rb
109
124
  - lib/vagrant_cloud/box/provider.rb
110
125
  - lib/vagrant_cloud/box/version.rb