conjur-api 4.13.0 → 4.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/CHANGELOG.md +8 -0
  4. data/Gemfile +0 -1
  5. data/README.md +59 -18
  6. data/conjur-api.gemspec +2 -1
  7. data/lib/conjur/acts_as_asset.rb +6 -9
  8. data/lib/conjur/acts_as_role.rb +54 -7
  9. data/lib/conjur/annotations.rb +52 -14
  10. data/lib/conjur/api/audit.rb +54 -8
  11. data/lib/conjur/api/authn.rb +54 -9
  12. data/lib/conjur/api/groups.rb +67 -4
  13. data/lib/conjur/api/hosts.rb +62 -3
  14. data/lib/conjur/api/layers.rb +48 -3
  15. data/lib/conjur/api/pubkeys.rb +140 -9
  16. data/lib/conjur/api.rb +13 -1
  17. data/lib/conjur/base.rb +93 -6
  18. data/lib/conjur/configuration.rb +247 -20
  19. data/lib/conjur/exists.rb +26 -0
  20. data/lib/conjur/group.rb +50 -4
  21. data/lib/conjur/has_attributes.rb +66 -5
  22. data/lib/conjur/resource.rb +155 -13
  23. data/lib/conjur/role.rb +1 -1
  24. data/lib/conjur/standard_methods.rb +7 -2
  25. data/lib/conjur/variable.rb +168 -6
  26. data/lib/conjur-api/version.rb +1 -1
  27. data/lib/conjur-api.rb +2 -0
  28. data/spec/api/authn_spec.rb +6 -3
  29. data/spec/api/groups_spec.rb +1 -1
  30. data/spec/api/pubkeys_spec.rb +4 -4
  31. data/spec/api/roles_spec.rb +4 -2
  32. data/spec/api/users_spec.rb +2 -2
  33. data/spec/api/variables_spec.rb +1 -1
  34. data/spec/helpers/errors_matcher.rb +34 -0
  35. data/spec/helpers/request_helpers.rb +10 -0
  36. data/spec/lib/annotations_spec.rb +5 -2
  37. data/spec/lib/audit_spec.rb +5 -5
  38. data/spec/lib/group_spec.rb +1 -1
  39. data/spec/lib/resource_spec.rb +11 -11
  40. data/spec/lib/role_spec.rb +8 -7
  41. data/spec/lib/user_spec.rb +7 -3
  42. data/spec/ssl_spec.rb +85 -0
  43. data/spec/standard_methods_helper.rb +2 -0
  44. data/spec/variable_spec.rb +5 -3
  45. metadata +34 -7
  46. data/lib/conjur/patches/rest-client.rb +0 -32
@@ -21,8 +21,42 @@
21
21
  module Conjur
22
22
 
23
23
  class << self
24
- # Sets the Configuration for the current thread, yields the block, then resets the thread-local variable.
25
- def with_configuration(config, &block)
24
+ # Saves the current thread local {Conjur::Configuration},
25
+ # sets the thread local {Conjur::Configuration} to `config`, yields to the block, and ensures that
26
+ # the original thread local configuration is restored.
27
+ #
28
+ # Because Conjur configuration is accessed from the 'global' {Conjur.configuration} method by all Conjur
29
+ # API methods, this method provides the ability to set a thread local value for use within the current,
30
+ # or within a block in a single threaded application.
31
+ #
32
+ # Note that the {Conjur.configuration=} method sets the *global* {Conjur::Configuration}, not the thread-local
33
+ # value.
34
+ #
35
+ # @example Override Configuration in a Thread
36
+ # # in this rather contrived example, we'll override the {Conjur::Configuration#appliance_url} parameter
37
+ # # used by calls within a thread.
38
+ #
39
+ # # Set up the configuration in the main thread
40
+ # Conjur.configure do |c|
41
+ # # ...
42
+ # c.appliance_url = 'https://conjur.main-url.com/api'
43
+ # end
44
+ #
45
+ # # Start a new thread that will perform requests to another server. In practice, you might
46
+ # # have a web server that uses a Conjur endpoint specified by a request header.
47
+ # Thread.new do
48
+ # Conjur.with_configuration Conjur.config.clone(appliance_url: 'https://conjur.local-url.com/api') do
49
+ # sleep 2
50
+ # puts "Thread local url is #{Conjur.config.appliance_url}"
51
+ # end
52
+ # end
53
+ # puts "Global url is #{Conjur.config.appliance_url}"
54
+ # # Outputs:
55
+ # Global url is https://conjur.main-url.com/api
56
+ # Thread local url is https://conjur.local-url.com/api
57
+ #
58
+ # @return [void]
59
+ def with_configuration(config)
26
60
  oldvalue = Thread.current[:conjur_configuration]
27
61
  Thread.current[:conjur_configuration] = config
28
62
  yield
@@ -31,26 +65,123 @@ module Conjur
31
65
  end
32
66
 
33
67
  # Gets the current thread-local or global configuration.
68
+ #
69
+ # The thread-local Conjur configuration can only be set using the {Conjur.with_configuration}
70
+ # method. This method will try to return that value first, then the global configuration as
71
+ # set with {Conjur.configuration=} (which is lazily initialized if not set).
72
+ #
73
+ # @return [Conjur::Configuration] the thread-local or global Conjur configuration.
34
74
  def configuration
35
75
  Thread.current[:conjur_configuration] || (@config ||= Configuration.new)
36
76
  end
37
77
 
38
78
  # Sets the global configuration.
79
+ #
80
+ # This method *has no effect* on the thread local configuration. Use {Conjur.with_configuration} instead if
81
+ # that's what you want.
82
+ #
83
+ # @param [Conjur::Configuration] config the new configuration
84
+ # @return [Conjur::Configuration] the new value of the configuration
39
85
  def configuration=(config)
40
86
  @config = config
41
87
  end
88
+
89
+ alias config configuration
90
+ alias config= configuration=
91
+
92
+ # Configure Conjur with a block.
93
+ #
94
+ # @example
95
+ # Conjur.configure do |c|
96
+ # c.account = 'some-account'
97
+ # c.appliance_url = 'https://conjur.companyname.com/api'
98
+ # end
99
+ #
100
+ # @yieldparam [Conjur::Configuration] c the configuration instance to modify.
101
+ def configure
102
+ yield configuration
103
+ end
42
104
  end
43
-
105
+
106
+ # Stores a configuration for the Conjur API client. This class provides *global* and *thread local* storage
107
+ # for common options used by the Conjur API. Most importantly, it specifies the
108
+ #
109
+ # * REST endpoints, derived from the {Conjur::Configuration#appliance_url} and {Conjur::Configuration#account} options
110
+ # * The certificate used for secure connections to the Conjur appliance ({Conjur::Configuration#cert_file})
111
+ #
112
+ # ### Environment Variables
113
+ #
114
+ # Option values used by Conjur can be given by environment variables, using a standard naming scheme. Specifically,
115
+ # an environment variable named `CONJUR_ACCOUNT` will be used to provide a default value for the {Conjur::Configuration#account}
116
+ # option.
117
+ #
118
+ #
119
+ # ### Required Options
120
+ #
121
+ # The {Conjur::Configuration#account} and {Conjur::Configuration#appliance_url} are always required. Except in
122
+ # special cases, the {Conjur::Configuration#cert_file} is also required, but you may omit it if your Conjur root
123
+ # certificate is in the OpenSSl default certificate store.
124
+ #
125
+ # ### Thread Local Configuration
126
+ #
127
+ # While using a globally available configuration is convenient for most applications, sometimes you will need to
128
+ # use different configurations in different threads. This is supported by returning a thread local version from {Conjur.configuration}
129
+ # if one has been set by {Conjur.with_configuration}.
130
+ #
131
+ # @see Conjur.configuration
132
+ # @see Conjur.configure
133
+ # @see Conjur.with_configuration
134
+ #
135
+ # @example Basic Configuration
136
+ # Conjur.configure do |c|
137
+ # c.account = 'the-account'
138
+ # c.cert_file = find_conjur_cert_file
139
+ # end
140
+ #
141
+ # @example Setting the appliance_url from an environment variable
142
+ # ENV['CONJUR_APPLIANCE_URL'] = 'https://some-host.com/api'
143
+ # Conjur::Configuration.new.appliance_url # => 'https://some-host.com/api'
144
+ #
145
+ # @example Using thread local configuration in a web application request handler
146
+ # # Assume that we're in a request handler thread in a multithreaded web server.
147
+ #
148
+ # requested_appliance_url = request.header 'X-Conjur-Appliance-Url'
149
+ #
150
+ # with_configuration Conjur.config.clone(appliance_url: requested_appliance_url) do
151
+ # # `api` is an instance attribute. Note that we can use an api that was created
152
+ # # before we modified the thread local configuration.
153
+ #
154
+ #
155
+ # # 404 if the user doesn't exist
156
+ #
157
+ # user = api.user request.header('X-Conjur-Login')
158
+ # raise HttpError, 404, "User #{user.login} does not exist" unless user.exists?
159
+ # # ... finish the request
160
+ # end
161
+ #
162
+ #
44
163
  class Configuration
45
- # All explicit values.
164
+ # @api private
46
165
  attr_reader :explicit
47
-
48
- # All explicit and cached values.
166
+
167
+ # @api private
49
168
  attr_reader :supplied
50
-
51
- def initialize explicit = {}
52
- @explicit = explicit.dup
53
- @supplied = explicit.dup
169
+
170
+
171
+ # Create a new {Conjur::Configuration}, setting initial values from
172
+ # `options`.
173
+ #
174
+ # @note `options` must use symbols for keys.
175
+ #
176
+ # @example
177
+ # Conjur.config = Conjur::Configuration.new account: 'companyname'
178
+ # Conjur.config.account # => 'companyname'
179
+ #
180
+ # @param [Hash] options hash of options to set on the new instance.
181
+ #
182
+ def initialize options = {}
183
+ @explicit = options.dup
184
+ @supplied = options.dup
54
185
  end
55
186
 
56
187
  class << self
@@ -112,44 +243,129 @@ module Conjur
112
243
  end
113
244
  end
114
245
 
115
- # Copies the current configuration, except a set of overridden options.
116
- def clone override_options
246
+ # Return a copy of this {Conjur::Configuration} instance, optionally
247
+ # updating the copy with options from the `override_options` hash.
248
+ #
249
+ # @example
250
+ # original = Conjur.configuration
251
+ # original.account # => 'conjur'
252
+ # copy = original.clone account: 'some-other-account'
253
+ # copy.account # => 'some-other-account'
254
+ # original.account # => 'conjur'
255
+ #
256
+ # @param [Hash] override_options options to set on the new instance
257
+ # @return [Conjur::Configuration] a copy of this configuration
258
+ def clone override_options = {}
117
259
  self.class.new self.explicit.dup.merge(override_options)
118
260
  end
119
261
 
262
+ # Manually set an option. Note that setting an option not present in
263
+ # {Conjur::Configuration.accepted_options} is a no op.
264
+ # @api private
265
+ # @param [Symbol, String] key the name of the option to set
266
+ # @param [Object] value the option value.
120
267
  def set(key, value)
121
268
  if self.class.accepted_options.include?(key.to_sym)
122
269
  explicit[key.to_sym] = value
123
270
  supplied[key.to_sym] = value
124
271
  end
125
272
  end
126
-
273
+
274
+ # @!attribute authn_url
275
+ # The url for the {http://developer.conjur.net/reference/services/authentication Conjur authentication service}.
276
+ #
277
+ # @note You should not generally set this value. Instead, Conjur will derive it from the
278
+ # {Conjur::Configuration#account} and {Conjur::Configuration#appliance_url}
279
+ # properties.
280
+ #
281
+ # @return [String] the authentication service url
127
282
  add_option :authn_url do
128
283
  account_service_url 'authn', 0
129
284
  end
130
-
285
+
286
+ # @!attribute authz_url
287
+ # The url for the {http://developer.conjur.net/reference/services/authorization Conjur authorization service}.
288
+ #
289
+ # @note You should not generally set this value. Instead, Conjur will derive it from the
290
+ # {Conjur::Configuration#account} and {Conjur::Configuration#appliance_url}
291
+ # properties.
292
+ #
293
+ # @return [String] the authorization service url
131
294
  add_option :authz_url do
132
295
  global_service_url 'authz', 100
133
296
  end
134
297
 
298
+ # @!attribute core_url
299
+ # The url for the {http://developer.conjur.net/reference/services/directory Conjur core/directory service}.
300
+ #
301
+ # @note You should not generally set this value. Instead, Conjur will derive it from the
302
+ # {Conjur::Configuration#account} and {Conjur::Configuration#appliance_url}
303
+ # properties.
304
+ #
305
+ # @return [String] the core/directory service url
135
306
  add_option :core_url do
136
307
  default_service_url 'core', 200
137
- end
138
-
308
+ end
309
+
310
+ # @!attribute audit_url
311
+ # The url for the {http://developer.conjur.net/reference/services/audit Conjur audit service}.
312
+ #
313
+ # @note You should not generally set this value. Instead, Conjur will derive it from the
314
+ # {Conjur::Configuration#account} and {Conjur::Configuration#appliance_url}
315
+ # properties.
316
+ #
317
+ # @return [String] the audit service url
139
318
  add_option :audit_url do
140
319
  global_service_url 'audit', 300
141
320
  end
142
-
321
+
322
+ # @!attribute appliance_url
323
+ # The url for your Conjur appliance.
324
+ #
325
+ # If your appliance's hostname is `'conjur.companyname.com'`, then your `appliance_url` will
326
+ # be `'https://conjur.companyname.com/api'`.
327
+ #
328
+ # @note If you are using an appliance (if you're not sure, you probably are), this option is *required*.
329
+ #
330
+ # @return [String] the appliance URL
143
331
  add_option :appliance_url
144
-
332
+
333
+ # NOTE DO NOT DOCUMENT THIS AS AN ATTRIBUTE, IT IS PRIVATE AND YARD DOESN'T SUPPORT @api private ON ATTRIBUTES.
334
+ #
335
+ # The port used to derive ports for conjur services running locally. You will only use this if you are
336
+ # running the Conjur services locally, in which case you are probably a Conjur developer, and should ask
337
+ # someone in chat ;-)
338
+ #
145
339
  add_option :service_base_port, default: 5000
146
340
 
341
+ # @!attribute account
342
+ # The organizational account used by Conjur.
343
+ #
344
+ # On Conjur appliances, this option will be set once when the appliance is first configured. You can get the
345
+ # value for the acccount option from your conjur administrator, or if you have installed
346
+ # the {http://developer.conjur.net/client_setup/cli.html Conjur command line tools} by running
347
+ # {http://developer.conjur.net/reference/services/authentication/whoami.html conjur authn whoami},
348
+ # or examining your {http://developer.conjur.net/client_setup/cli.html#Configure .conjurrc file}.
349
+ #
350
+ # @note this option is **required**, and attempting to make any api calls prior to setting it (either
351
+ # explicitly or with the `"CONJUR_ACCOUNT"` environment variable) will raise an exception.
352
+ #
353
+ # @return [String]
147
354
  add_option :account, required: true
148
-
355
+
356
+
357
+ # @!attribute env
358
+ #
359
+ # The type of environment your program is running in (e.g., `development`, `production`, `test`).
360
+ #
361
+ # @deprecated
362
+ #
363
+ # @return [String] the environment name
149
364
  add_option :env do
150
365
  ENV['CONJUR_ENV'] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "production"
151
366
  end
152
-
367
+
368
+ # DEPRECATED SaaS option, do not doc comment!
153
369
  add_option :stack do
154
370
  case env
155
371
  when "production"
@@ -159,6 +375,17 @@ module Conjur
159
375
  end
160
376
  end
161
377
 
378
+ # @!attribute cert_file
379
+ #
380
+ # Path to the certificate file to use when making secure connections to your Conjur appliance.
381
+ #
382
+ # This should be the path to the root Conjur SSL certificate in PEM format. You will normally get the
383
+ # certificate file using the {http://developer.conjur.net/reference/tools/utilities/init.html conjur init} command.
384
+ # This option is not required if the certificate or its root is in the OpenSSL default cert store.
385
+ # If your program throws an error indicating that SSL verification has failed, you probably need
386
+ # to set or fix this option.
387
+ #
388
+ # @return [String, nil] path to the certificate file, or nil if you aren't using one.
162
389
  add_option :cert_file
163
390
 
164
391
  private
data/lib/conjur/exists.rb CHANGED
@@ -19,7 +19,33 @@
19
19
  # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
20
  #
21
21
  module Conjur
22
+ # Provides an `exists?` method for things that may or may not exist.
23
+ #
24
+ #
25
+ # Most conjur assets returned by `api.asset_name` methods (e.g., {Conjur::API#group}, {Conjur::API#user})
26
+ # may or may not exist. The {Conjur::Exists#exists?} method lets you determine whether or not such assets
27
+ # do in fact exist.
22
28
  module Exists
29
+
30
+ # Check whether this asset exists by performing a HEAD request to its URL.
31
+ #
32
+ # This method will return false if the asset doesn't exist.
33
+ #
34
+ # @example
35
+ # does_not_exist = api.user 'does-not-exist' # This returns without error.
36
+ #
37
+ # # this is wrong!
38
+ # owner = does_not_exist.ownerid # raises RestClient::ResourceNotFound
39
+ #
40
+ # # this is right!
41
+ # owner = if does_not_exist.exists?
42
+ # does_not_exist.ownerid
43
+ # else
44
+ # nil # or some sensible default
45
+ # end
46
+ #
47
+ # @param [Hash] options included for compatibility: **don't use this argument**!
48
+ # @return [Boolean] does it exist?
23
49
  def exists?(options = {})
24
50
  begin
25
51
  self.head(options)
data/lib/conjur/group.rb CHANGED
@@ -18,21 +18,67 @@
18
18
  # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19
19
  #
20
20
  module Conjur
21
+
22
+ # A Conjur {http://developer.conjur.net/reference/services/directory/group Group} represents a collection of
23
+ # Conjur {http://developer.conjur.net/reference/services/directory/user Users}.
24
+ # This class represents Conjur group assets and operations on them.
25
+ #
26
+ # You should not create instances of this class directly. Instead, you can get them from
27
+ # API methods like {Conjur::API#group} and {Conjur::API#groups}.
28
+ #
21
29
  class Group < RestClient::Resource
22
30
  include ActsAsAsset
23
31
  include ActsAsRole
24
-
32
+
33
+ # Add a user to the group or change whether an existing member can manage other members.
34
+ #
35
+ # @example
36
+ # # create an empty group
37
+ # group = api.create_group 'hoommans'
38
+ # # put a user in the group, with the ability to manage members
39
+ # group.add_member 'conjur:user:bob', admin_option: True
40
+ # # Hmm, bob is getting a little suspicious, better lower his privileges.
41
+ # group.add_member 'conjur:user:bob', admin_option: False
42
+ #
43
+ # # Notice that this method is idempotent:
44
+ # group.add_member 'alice'
45
+ # group.add_member 'alice' # Does nothing, alice is already a member
46
+ #
47
+ #
48
+ # @param [String, Conjur::User, Conjur::Role] member the member to add. If a String is given, it must
49
+ # be a *fully qualified* Conjur id.
25
50
  # @param [Hash] options
26
- # * *admin_option* enables the +member+ to manage members of this group
51
+ # @option options [Boolean] :admin_option (False) determines whether the member is able to manage members
52
+ # of this group.
53
+ # @return [void]
27
54
  def add_member(member, options = {})
28
55
  role.grant_to member, options
29
56
  end
30
-
57
+
58
+ # Remove a member from this group.
59
+ #
60
+ # ### Notes
61
+ # * Unlike {#add_member}, this method is *not* idempotent.
62
+ # This means that calling it twice with the same user will raise a `RestClient::ResourceNotFound`
63
+ # exception.
64
+ # * The member may be represented as a *qualified* conjur id or a {Conjur::User} instance. Although
65
+ # it will accept anything that responds to `#roleid`, the behavior when adding or removing a non-user
66
+ # role is **undefined**.
67
+ #
68
+ # @example
69
+ # group = api.group 'admins'
70
+ # group.add_member 'bob'
71
+ # group.remove_member 'bob' # OK, bob is a member
72
+ # group.remove_member 'bob' # raises RestClient::ResourceNotFound
73
+ #
74
+ # @param [String, Conjur::User,Conjur::Role] member
75
+ # @return [void]
76
+ # @raise [RestClient::ResourceNotFound] when you try to remove a user who is not a member of the group.
31
77
  def remove_member(member)
32
78
  role.revoke_from member
33
79
  end
34
80
 
35
- # Update group properties
81
+ # Update group properties. Currently the only supported property is `:gidnumber`.
36
82
  #
37
83
  # @param [Hash] props new property values
38
84
  # @option props [Integer] :gidnumber new GID number
@@ -19,27 +19,87 @@
19
19
  # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
20
  #
21
21
  module Conjur
22
+ # Many Conjur assets have key-value attributes. Although these should generally be accessed via
23
+ # methods on specific asset classes (for example, {Conjur::Resource#owner}), the are available as
24
+ # a `Hash` on all types supporting attributes.
22
25
  module HasAttributes
26
+ # Returns this objects {#attributes}. This is primarily to support
27
+ # simple JSON serialization of Conjur assets.
28
+ #
29
+ # @param options [Hash,nil] unused, kept for compatibility reasons
30
+ # @see #attributes
23
31
  def to_json(options = {})
24
32
  attributes
25
33
  end
26
34
 
27
- def attributes=(a); @attributes = a; end
35
+ # @api private
36
+ # Set the attributes for this Resource.
37
+ # @param [Hash] attributes new attributes for the object.
38
+ # @return [Hash] the new attributes
39
+ def attributes=(attributes); @attributes = attributes; end
40
+
41
+ # Get the attributes for this asset.
42
+ #
43
+ # Although the `Hash` returned by this method is mutable, you should treat as immutable unless you know
44
+ # exactly what you're doing. Each asset's attributes are constrained by a server side schema, which means
45
+ # that you will get an error if you violate the schema. and then try to save the asset.
46
+ #
47
+ #
48
+ # @note this method will use a cached copy of the objects attributes instead of fetching them
49
+ # with each call. To ensure that the attributes are fresh, you can use the {#refresh} method
50
+ #
51
+ # @return [Hash] the asset's attributes.
28
52
  def attributes
29
53
  return @attributes if @attributes
30
54
  fetch
31
55
  end
32
-
56
+
57
+
58
+ # Update this asset's attributes on the server.
59
+ #
60
+ #
61
+ # @note If the objects attributes haven't been fetched (for example, by calling {#attributes}),
62
+ # this method is a no-op.
63
+ #
64
+ # Although you can manipulate an assets attributes and then call {#save}, the attributes are constrained
65
+ # by a server side schema, and attempting to set an attribute that doesn't exist will result in
66
+ # a 422 Unprocessable Entity error.
67
+ #
68
+ # If you want to set arbitrary metadata on an asset, you might consider using the {Conjur::Resource#tags}
69
+ # method instead.
70
+ #
71
+ # @return [void]
33
72
  def save
34
- self.put(attributes.to_json)
73
+ if @attributes
74
+ self.put(attributes.to_json)
75
+ end
35
76
  end
36
77
 
37
- # Reload the attributes. This action can be used to guarantee a current view of the entity in the case
78
+ # Reload this asset's attributes. This method can be used to guarantee a current view of the entity in the case
38
79
  # that it has been modified by an update method or by an external party.
80
+ #
81
+ # @note any changes to {#attributes} without a call to #save will be overwritten by this method.
82
+ #
83
+ # @example
84
+ # res = api.resources.firs
85
+ # res.attributes # => { ... }
86
+ # res.attributes['hello'] = 'blah'
87
+ # res.refresh
88
+ # res.attributes['hello'] # => nil
89
+ #
90
+ #
91
+ # @return [Hash] the asset's attributes.
39
92
  def refresh
40
93
  fetch
41
94
  end
42
95
 
96
+ # Call a block that will perform actions that might change the asset's attributes.
97
+ # No matter what happens in the block, this method ensures that the cached attributes
98
+ # will be invalidated.
99
+ #
100
+ # @note this is mainly used internally, but included in the public api for completeness.
101
+ #
102
+ # @return [void]
43
103
  def invalidate(&block)
44
104
  yield
45
105
  ensure
@@ -47,7 +107,8 @@ module Conjur
47
107
  end
48
108
 
49
109
  protected
50
-
110
+ # @api private
111
+ # Fetch the attributes, overwriting any current ones.
51
112
  def fetch
52
113
  @attributes = JSON.parse(get.body)
53
114
  end