esp_sdk 2.5.0 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.yardopts +1 -0
  4. data/CHANGELOG.md +8 -0
  5. data/Gemfile.lock +5 -3
  6. data/Guardfile +3 -1
  7. data/esp_sdk.gemspec +2 -1
  8. data/lib/esp/aws_clients.rb +2 -1
  9. data/lib/esp/commands/commands_tasks.rb +2 -1
  10. data/lib/esp/commands/console.rb +4 -0
  11. data/lib/esp/exceptions.rb +1 -0
  12. data/lib/esp/extensions/active_resource/dirty.rb +51 -0
  13. data/lib/esp/extensions/active_resource/formats/json_api_format.rb +5 -3
  14. data/lib/esp/extensions/active_resource/paginated_collection.rb +71 -38
  15. data/lib/esp/extensions/active_resource/validations.rb +4 -2
  16. data/lib/esp/external_account_creator.rb +4 -1
  17. data/lib/esp/resources/alert.rb +53 -42
  18. data/lib/esp/resources/cloud_trail_event.rb +18 -12
  19. data/lib/esp/resources/concerns/stat_totals.rb +70 -67
  20. data/lib/esp/resources/contact_request.rb +17 -14
  21. data/lib/esp/resources/custom_signature/definition.rb +46 -51
  22. data/lib/esp/resources/custom_signature/result/alert.rb +13 -5
  23. data/lib/esp/resources/custom_signature/result.rb +49 -53
  24. data/lib/esp/resources/custom_signature.rb +52 -61
  25. data/lib/esp/resources/dashboard.rb +11 -5
  26. data/lib/esp/resources/external_account.rb +59 -58
  27. data/lib/esp/resources/metadata.rb +21 -11
  28. data/lib/esp/resources/organization.rb +49 -39
  29. data/lib/esp/resources/region.rb +25 -28
  30. data/lib/esp/resources/report.rb +46 -44
  31. data/lib/esp/resources/reports/export/integration.rb +22 -13
  32. data/lib/esp/resources/resource.rb +4 -3
  33. data/lib/esp/resources/scan_interval.rb +19 -13
  34. data/lib/esp/resources/service.rb +17 -11
  35. data/lib/esp/resources/signature.rb +43 -53
  36. data/lib/esp/resources/stat.rb +72 -55
  37. data/lib/esp/resources/stat_custom_signature.rb +73 -65
  38. data/lib/esp/resources/stat_region.rb +76 -65
  39. data/lib/esp/resources/stat_service.rb +76 -65
  40. data/lib/esp/resources/stat_signature.rb +76 -65
  41. data/lib/esp/resources/sub_organization.rb +51 -60
  42. data/lib/esp/resources/suppression/region.rb +35 -30
  43. data/lib/esp/resources/suppression/signature.rb +35 -29
  44. data/lib/esp/resources/suppression/unique_identifier.rb +27 -22
  45. data/lib/esp/resources/suppression.rb +45 -34
  46. data/lib/esp/resources/tag.rb +20 -11
  47. data/lib/esp/resources/team.rb +56 -58
  48. data/lib/esp/resources/user.rb +35 -32
  49. data/lib/esp/version.rb +1 -1
  50. data/lib/esp.rb +39 -16
  51. data/lib/esp_sdk.rb +1 -0
  52. data/test/esp/extensions/active_resource/dirty_test.rb +81 -0
  53. data/test/esp/extensions/active_resource/formats/json_api_format_test.rb +8 -0
  54. data/test/esp/extensions/active_resource/paginated_collection_test.rb +7 -0
  55. data/test/esp/integration/json_api_format_integration_test.rb +5 -2
  56. data/test/esp/integration/organization_integration_test.rb +1 -1
  57. data/test/esp/resources/custom_signature_test.rb +15 -0
  58. data/test/factories/custom_signatures.rb +0 -10
  59. metadata +21 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 985f05618a6621f6fd81b455b13b532c91712638
4
- data.tar.gz: 773b7274d4889449a4264f8f19e869f4b9830798
3
+ metadata.gz: ee71a8ce107cfd0be1c552e60e32ea3637c21eaa
4
+ data.tar.gz: f4087d8db9e1affb4f96a1eceae43ba3211673bf
5
5
  SHA512:
6
- metadata.gz: 54b0c9606230136eff0b3c463467be3f9eebf1986b4e603622c1efa59e680496769b7d086ae13913be912df2c12e7ac2f1e5352e39a1ba18628ffc4c9e36828a
7
- data.tar.gz: c398c98653b66c5adac110c958580662fb1bba451fa0d8db98cc6f435e6d1ed0f5f79c594a768bee60232d9b9799eeff230c65961ab1c4cc43e2917bb344667e
6
+ metadata.gz: b3f490d1f67a732b9b85daf1036d69af6b8295238ce9eb255b41f4fd10172106663e7e3bd7c7f45c7d0c12219b1a34bf68690bd0c12285c1cd25198fcf26aacd
7
+ data.tar.gz: 9ece944bf4867f36e4df3457e12c739b15d79dd0ebc729d9b5d2d47955e941b6b3e2042ef88a0fd9eec9fc35d77f052c113342bbbd06c346d1549dfcffb89f93
data/.gitignore CHANGED
@@ -17,5 +17,7 @@ tmp
17
17
  *.so
18
18
  *.o
19
19
  *.a
20
+ *.swp
21
+ *.swo
20
22
  mkmf.log
21
23
  .idea
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --no-private
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## Unreleased
2
2
 
3
+ ## 2.6.0 - 2016-10-17
4
+ ### Changed
5
+ - Only send changed attributes to API #39
6
+ - API now returns nested included relation data elements correctly. Change test to reflect corrected response. #40
7
+ - Calling `next_page` on queries without parameters (e.g. `ESP::ExternalAccount.all`) no longer errors. #35
8
+ - Silently ignore `null` entires if encountered in API responses. #69
9
+ - Switch from RDoc to Yard. #37
10
+
3
11
  ## 2.5.0 - 2016-07-20
4
12
  ### Added
5
13
  - Add custom signature definitions and results. Code for a custom signature is now created/updated under a definition. Running a definition for an on demand test creates a result record which will have errors and alerts when completed.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- esp_sdk (2.5.0)
4
+ esp_sdk (2.6.0)
5
5
  activeresource (~> 4.0.0)
6
6
  api-auth (~> 2.0.0)
7
7
  rack
@@ -107,7 +107,7 @@ GEM
107
107
  rb-fsevent (0.9.6)
108
108
  rb-inotify (0.9.5)
109
109
  ffi (>= 0.5.0)
110
- rdoc (4.0.0)
110
+ rdiscount (2.2.0.1)
111
111
  rest-client (1.7.3)
112
112
  mime-types (>= 1.16, < 3.0)
113
113
  netrc (~> 0.7)
@@ -142,6 +142,7 @@ GEM
142
142
  webmock (1.21.0)
143
143
  addressable (>= 2.3.6)
144
144
  crack (>= 0.3.2)
145
+ yard (0.9.5)
145
146
 
146
147
  PLATFORMS
147
148
  ruby
@@ -161,10 +162,11 @@ DEPENDENCIES
161
162
  minitest-reporters
162
163
  mocha
163
164
  rake
164
- rdoc
165
+ rdiscount
165
166
  rubocop
166
167
  shoulda
167
168
  webmock
169
+ yard
168
170
 
169
171
  BUNDLED WITH
170
172
  1.12.5
data/Guardfile CHANGED
@@ -15,10 +15,12 @@
15
15
  #
16
16
  # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
17
17
 
18
- guard :minitest do
18
+ guard :minitest, all_on_start: false do
19
19
  # with Minitest::Unit
20
20
  watch(%r{^test/(.*)\/?test_(.*)\.rb$})
21
+ watch(%r{^test/.+_test\.rb$})
21
22
  watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}test_#{m[2]}.rb" }
23
+ watch(%r{^lib/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" }
22
24
  watch(%r{^test/test_helper\.rb$}) { 'test' }
23
25
 
24
26
  # with Minitest::Spec
data/esp_sdk.gemspec CHANGED
@@ -33,9 +33,10 @@ Gem::Specification.new do |spec|
33
33
  spec.add_development_dependency 'webmock'
34
34
  spec.add_development_dependency 'coveralls'
35
35
  spec.add_development_dependency 'factory_girl'
36
- spec.add_development_dependency 'rdoc'
36
+ spec.add_development_dependency 'yard'
37
37
  spec.add_development_dependency 'awesome_print'
38
38
  spec.add_development_dependency 'aws-sdk'
39
+ spec.add_development_dependency 'rdiscount'
39
40
 
40
41
  spec.add_dependency 'activeresource', '~> 4.0.0'
41
42
  spec.add_dependency 'api-auth', '~> 2.0.0'
@@ -1,6 +1,7 @@
1
1
  require 'aws-sdk'
2
2
 
3
- module ESP # :nodoc: all
3
+ module ESP
4
+ # @private
4
5
  class AWSClients
5
6
  include ActiveModel::Validations
6
7
 
@@ -4,7 +4,8 @@ module ESP
4
4
  #
5
5
  # Warning: This class mutates ARGV because some commands require manipulating
6
6
  # it before they are run.
7
- class CommandsTasks # :nodoc:
7
+ # @private
8
+ class CommandsTasks
8
9
  attr_reader :argv
9
10
 
10
11
  HELP_MESSAGE = <<-EOT
@@ -21,7 +21,11 @@ ARGV.clone.options do |opts|
21
21
  end
22
22
 
23
23
  module ESP
24
+ # @private
24
25
  class Console
26
+ # Start a console
27
+ #
28
+ # @return [void]
25
29
  def start # rubocop:disable Metrics/MethodLength
26
30
  ARGV.clear
27
31
  IRB.setup nil
@@ -1,3 +1,4 @@
1
1
  module ESP
2
+ # @private
2
3
  class NotImplementedError < StandardError; end
3
4
  end
@@ -0,0 +1,51 @@
1
+ module Dirty
2
+ def original_attributes
3
+ @original_attributes ||= {}.with_indifferent_access
4
+ end
5
+
6
+ def original_attributes=(attributes = {})
7
+ @original_attributes = attributes.dup
8
+ end
9
+
10
+ def changed_attributes
11
+ attributes.select do |key, value|
12
+ next if value == original_attributes[key]
13
+ true
14
+ end
15
+ end
16
+ end
17
+
18
+ # Set the original attributes every time we instantiate an object from the api
19
+ # This happens on GET requests
20
+ module InstantiateWithOriginalAttributes
21
+ private
22
+
23
+ def instantiate_record(record, _prefix_options = {})
24
+ super(record, _prefix_options = {}).tap do |object|
25
+ object.original_attributes = object.attributes
26
+ end
27
+ end
28
+ end
29
+
30
+ module LoadWithOriginalAttributes
31
+ # After sending to the API the object is reloaded with its attributes
32
+ # The persisted flag tells us it has been saved
33
+ def load(attributes, _remove_root = false, persisted = false)
34
+ if persisted
35
+ super.tap do |object|
36
+ object.original_attributes = object.attributes
37
+ end
38
+ else
39
+ super
40
+ end
41
+ end
42
+ end
43
+
44
+ class ActiveResource::Base
45
+ include Dirty
46
+ prepend LoadWithOriginalAttributes
47
+
48
+ class << self
49
+ prepend InstantiateWithOriginalAttributes
50
+ end
51
+ end
@@ -1,6 +1,7 @@
1
1
  require 'active_support/json'
2
2
 
3
- module ActiveResource # :nodoc: all
3
+ module ActiveResource
4
+ # @private
4
5
  class ConnectionError
5
6
  def initialize(response)
6
7
  @response = if response.respond_to?(:response)
@@ -20,7 +21,8 @@ module ActiveResource # :nodoc: all
20
21
  end
21
22
  end
22
23
 
23
- module Formats # :nodoc: all
24
+ # @private
25
+ module Formats
24
26
  module JsonAPIFormat
25
27
  module_function
26
28
 
@@ -121,7 +123,7 @@ module ActiveResource # :nodoc: all
121
123
  end
122
124
 
123
125
  def self.merge_nested_included_objects(object, data, included)
124
- assocs = included.select { |i| data.include?((i.slice('type', 'id'))) }
126
+ assocs = included.compact.select { |i| data.include?(i.slice('type', 'id')) }
125
127
  # Remove the object from the included array to prevent an infinite loop if one of it's associations relates back to itself.
126
128
  assoc_included = included.dup
127
129
  assoc_included.delete(object)
@@ -1,12 +1,18 @@
1
1
  require 'rack'
2
2
 
3
3
  module ActiveResource
4
- # Provides a mean to call the Evident.io API to easily retrieve paginated data
5
4
  class PaginatedCollection < ActiveResource::Collection
6
- attr_reader :next_page_params, :previous_page_params, :last_page_params #:nodoc:
7
- attr_accessor :from #:nodoc:
8
-
9
- def initialize(elements = []) #:nodoc:
5
+ # Internal variable used to construct queries.
6
+ # @return [Hash]
7
+ # @private
8
+ attr_reader :next_page_params, :previous_page_params, :last_page_params
9
+ # Internal variable used to construct queries.
10
+ # @return [Integer]
11
+ # @private
12
+ attr_accessor :from
13
+
14
+ # @private
15
+ def initialize(elements = [])
10
16
  # If a collection is sent without the pagination links, then elements will just be an array.
11
17
  if elements.is_a? Hash
12
18
  super(elements['data'])
@@ -16,22 +22,24 @@ module ActiveResource
16
22
  end
17
23
  end
18
24
 
19
- # Returns a new PaginatedCollection with the first page of results.
25
+ # Returns the first page of results.
20
26
  #
21
- # Returns self when on the first page and no API call is made.
27
+ # Returns +self+ (and no API call is made) when already on the first page.
22
28
  #
23
- # ==== Example
29
+ # @return [PaginatedCollection, self]
30
+ # @example
24
31
  # alerts.current_page_number # => 5
25
32
  # first_page = alerts.first_page
26
33
  # alerts.current_page_number # => 5
27
34
  # first_page.current_page_number # => 1
28
35
  def first_page
29
- previous_page? ? resource_class.where(original_params.merge(from: from, page: { number: 1 })) : self
36
+ previous_page? ? updated_collection(from: from, page: { number: 1 }) : self
30
37
  end
31
38
 
32
39
  # Updates the existing PaginatedCollection object with the first page of data when not on the first page.
33
40
  #
34
- # ==== Example
41
+ # @return (see #first_page)
42
+ # @example
35
43
  # alerts.current_page_number # => 5
36
44
  # alerts.first_page!
37
45
  # alerts.current_page_number # => 1
@@ -39,22 +47,24 @@ module ActiveResource
39
47
  first_page.tap { |page| update_self(page) }
40
48
  end
41
49
 
42
- # Returns a new PaginatedCollection with the previous page of results.
50
+ # Returns the previous page of results.
43
51
  #
44
- # Returns self when on the first page and no API call is made.
52
+ # Returns +self+ (and no API call is made) when already on the first page.
45
53
  #
46
- # ==== Example
54
+ # @return [PaginatedCollection, self]
55
+ # @example
47
56
  # alerts.current_page_number # => 5
48
57
  # previous_page = alerts.previous_page
49
58
  # alerts.current_page_number # => 5
50
59
  # previous_page.current_page_number # => 4
51
60
  def previous_page
52
- previous_page? ? resource_class.where(original_params.merge(previous_page_params.merge(from: from))) : self
61
+ previous_page? ? updated_collection(previous_page_params.merge(from: from)) : self
53
62
  end
54
63
 
55
64
  # Updates the existing PaginatedCollection object with the previous page of data when not on the first page.
56
65
  #
57
- # ==== Example
66
+ # @return (see #previous_page)
67
+ # @example
58
68
  # alerts.current_page_number # => 5
59
69
  # alerts.previous_page!
60
70
  # alerts.current_page_number # => 4
@@ -62,22 +72,24 @@ module ActiveResource
62
72
  previous_page.tap { |page| update_self(page) }
63
73
  end
64
74
 
65
- # Returns a new PaginatedCollection with the next page of results.
75
+ # Returns the next page of results.
66
76
  #
67
- # Returns self when on the last page and no API call is made.
77
+ # Returns +self+ (and no API call is made) when already on the last page.
68
78
  #
69
- # ==== Example
79
+ # @return [PaginatedCollection, self]
80
+ # @example
70
81
  # alerts.current_page_number # => 5
71
82
  # next_page = alerts.next_page
72
83
  # alerts.current_page_number # => 5
73
84
  # next_page.current_page_number # => 6
74
85
  def next_page
75
- next_page? ? resource_class.where(original_params.merge(next_page_params.merge(from: from))) : self
86
+ next_page? ? updated_collection(next_page_params.merge(from: from)) : self
76
87
  end
77
88
 
78
89
  # Updates the existing PaginatedCollection object with the last page of data when not on the last page.
79
90
  #
80
- # ==== Example
91
+ # @return (see #next_page)
92
+ # @example
81
93
  # alerts.current_page_number # => 5
82
94
  # alerts.next_page!
83
95
  # alerts.current_page_number # => 6
@@ -85,22 +97,24 @@ module ActiveResource
85
97
  next_page.tap { |page| update_self(page) }
86
98
  end
87
99
 
88
- # Returns a new PaginatedCollection with the last page of results.
100
+ # Returns the last page of results.
89
101
  #
90
- # Returns self when on the last page and no API call is made.
102
+ # Returns +self+ (and no API call is made) when already on the last page.
91
103
  #
92
- # ==== Example
104
+ # @return [PaginatedCollection, self]
105
+ # @example
93
106
  # alerts.current_page_number # => 5
94
107
  # last_page = alerts.last_page
95
108
  # alerts.current_page_number # => 5
96
109
  # last_page.current_page_number # => 25
97
110
  def last_page
98
- !last_page? ? resource_class.where(original_params.merge(last_page_params.merge(from: from))) : self
111
+ !last_page? ? updated_collection(last_page_params.merge(from: from)) : self
99
112
  end
100
113
 
101
114
  # Updates the existing PaginatedCollection object with the last page of data when not on the last page.
102
115
  #
103
- # ==== Example
116
+ # @return (see #last_page)
117
+ # @example
104
118
  # alerts.current_page_number # => 5
105
119
  # alerts.last_page!
106
120
  # alerts.current_page_number # => 25
@@ -108,15 +122,14 @@ module ActiveResource
108
122
  last_page.tap { |page| update_self(page) }
109
123
  end
110
124
 
111
- # Returns a new PaginatedCollection with the +page_number+ page of data.
112
- #
113
- # Returns self when +page_number+ == #current_page_number
125
+ # Returns the +page_number+ page of data.
114
126
  #
115
- # ==== Attribute
127
+ # Returns +self+ when +page_number+ == +#current_page_number+
116
128
  #
117
- # +page_number+ - The page number of the data wanted. +page_number+ must be between 1 and #last_page_number.
118
- #
119
- # ==== Example
129
+ # @param page_number [Integer] The page number of the data wanted. Must be between 1 and +#last_page_number+.
130
+ # @return [PaginatedCollection, self]
131
+ # @raise [ArgumentError] if no page number or an out-of-bounds page number is supplied.
132
+ # @example
120
133
  # alerts.current_page_number # => 5
121
134
  # page = alerts.page(2)
122
135
  # alerts.current_page_number # => 5
@@ -125,16 +138,14 @@ module ActiveResource
125
138
  fail ArgumentError, "You must supply a page number." unless page_number.present?
126
139
  fail ArgumentError, "Page number cannot be less than 1." if page_number.to_i < 1
127
140
  fail ArgumentError, "Page number cannot be greater than the last page number." if page_number.to_i > last_page_number.to_i
128
- page_number.to_i != current_page_number.to_i ? resource_class.where(original_params.merge(from: from, page: { number: page_number, size: (next_page_params || previous_page_params)['page']['size'] })) : self
141
+ page_number.to_i != current_page_number.to_i ? updated_collection(from: from, page: { number: page_number, size: (next_page_params || previous_page_params)['page']['size'] }) : self
129
142
  end
130
143
 
131
144
  # Returns a new PaginatedCollection with the +page_number+ page of data when not already on page +page_number+.
132
145
  #
133
- # ==== Attribute
134
- #
135
- # +page_number+ - The page number of the data wanted. +page_number+ must be between 1 and #last_page_number.
136
- #
137
- # ==== Example
146
+ # @param (see #page)
147
+ # @return (see #page)
148
+ # @example
138
149
  # alerts.current_page_number # => 5
139
150
  # alerts.page!(2)
140
151
  # alerts.current_page_number # => 2
@@ -143,42 +154,64 @@ module ActiveResource
143
154
  end
144
155
 
145
156
  # The current page number of data.
157
+ #
158
+ # @return [String]
146
159
  def current_page_number
147
160
  (previous_page_number.to_i + 1).to_s
148
161
  end
149
162
 
150
163
  # The previous page number of data.
164
+ #
165
+ # @return [String, nil]
151
166
  def previous_page_number
152
167
  Hash(previous_page_params).fetch('page', {}).fetch('number', nil)
153
168
  end
154
169
 
155
170
  # The next page number of data.
171
+ #
172
+ # @return [String, nil]
156
173
  def next_page_number
157
174
  Hash(next_page_params).fetch('page', {}).fetch('number', nil)
158
175
  end
159
176
 
160
177
  # The last page number of data.
178
+ #
179
+ # @return [String, nil]
161
180
  def last_page_number
162
181
  Hash(last_page_params).fetch('page', {}).fetch('number', nil)
163
182
  end
164
183
 
165
184
  # Returns whether or not there is a previous page of data in the collection.
185
+ #
186
+ # @return [Boolean]
166
187
  def previous_page?
167
188
  !previous_page_number.nil?
168
189
  end
169
190
 
170
191
  # Returns whether or not there is a next page of data in the collection.
192
+ #
193
+ # @return [Boolean]
171
194
  def next_page?
172
195
  !next_page_number.nil?
173
196
  end
174
197
 
175
198
  # Returns whether or not the collection is on the last page.
199
+ #
200
+ # @return [Boolean]
176
201
  def last_page?
177
202
  last_page_number.nil?
178
203
  end
179
204
 
180
205
  private
181
206
 
207
+ # Start a new collection.
208
+ #
209
+ # @param params [Hash]
210
+ # @return [PaginatedCollection]
211
+ def updated_collection(params)
212
+ resource_class.where(original_params.merge(params))
213
+ end
214
+
182
215
  def update_self(page)
183
216
  @elements = page.elements
184
217
  @next_page_params = page.next_page_params
@@ -1,8 +1,9 @@
1
- module ActiveResource # :nodoc: all
1
+ module ActiveResource
2
+ # @private
2
3
  module Validations
3
4
  # Loads the set of remote errors into the object's Errors based on the
4
5
  # content-type of the error-block received.
5
- def load_remote_errors(remote_errors, save_cache = false) #:nodoc:
6
+ def load_remote_errors(remote_errors, save_cache = false)
6
7
  if self.class.format == ActiveResource::Formats::JsonAPIFormat
7
8
  errors.from_json_api(remote_errors.response.body, save_cache)
8
9
  elsif self.class.format == ActiveResource::Formats[:json]
@@ -11,6 +12,7 @@ module ActiveResource # :nodoc: all
11
12
  end
12
13
  end
13
14
 
15
+ # @private
14
16
  class Errors
15
17
  def from_json_api(json, save_cache = false)
16
18
  raw_errors = decoded_errors(json)
@@ -1,4 +1,5 @@
1
- module ESP # :nodoc: all
1
+ module ESP
2
+ # @private
2
3
  class AddExternalAccountError < StandardError
3
4
  EXIT_CODES = {
4
5
  '12 characters' => 98,
@@ -20,6 +21,7 @@ module ESP # :nodoc: all
20
21
  end
21
22
  end
22
23
 
24
+ # @private
23
25
  class ExternalAccountCreator
24
26
  attr_reader :aws
25
27
 
@@ -27,6 +29,7 @@ module ESP # :nodoc: all
27
29
  @aws = AWSClients.new
28
30
  end
29
31
 
32
+ # @return [ESP::ExternalAccount]
30
33
  def create
31
34
  fail ESP::AddExternalAccountError, aws.errors.full_messages.join(', ') unless aws.valid?
32
35