ocean-rails 1.14.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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +72 -0
  4. data/Rakefile +38 -0
  5. data/lib/generators/ocean_scaffold/USAGE +8 -0
  6. data/lib/generators/ocean_scaffold/ocean_scaffold_generator.rb +76 -0
  7. data/lib/generators/ocean_scaffold/templates/controller_specs/create_spec.rb +71 -0
  8. data/lib/generators/ocean_scaffold/templates/controller_specs/delete_spec.rb +47 -0
  9. data/lib/generators/ocean_scaffold/templates/controller_specs/index_spec.rb +45 -0
  10. data/lib/generators/ocean_scaffold/templates/controller_specs/show_spec.rb +43 -0
  11. data/lib/generators/ocean_scaffold/templates/controller_specs/update_spec.rb +85 -0
  12. data/lib/generators/ocean_scaffold/templates/model_spec.rb +76 -0
  13. data/lib/generators/ocean_scaffold/templates/resource_routing_spec.rb +27 -0
  14. data/lib/generators/ocean_scaffold/templates/view_specs/_resource_spec.rb +55 -0
  15. data/lib/generators/ocean_scaffold/templates/views/_resource.json.jbuilder +8 -0
  16. data/lib/generators/ocean_setup/USAGE +8 -0
  17. data/lib/generators/ocean_setup/ocean_setup_generator.rb +93 -0
  18. data/lib/generators/ocean_setup/templates/Gemfile +19 -0
  19. data/lib/generators/ocean_setup/templates/alive_controller.rb +18 -0
  20. data/lib/generators/ocean_setup/templates/alive_routing_spec.rb +11 -0
  21. data/lib/generators/ocean_setup/templates/alive_spec.rb +12 -0
  22. data/lib/generators/ocean_setup/templates/api_constants.rb +19 -0
  23. data/lib/generators/ocean_setup/templates/application_controller.rb +8 -0
  24. data/lib/generators/ocean_setup/templates/application_helper.rb +34 -0
  25. data/lib/generators/ocean_setup/templates/config.yml.example +57 -0
  26. data/lib/generators/ocean_setup/templates/errors_controller.rb +14 -0
  27. data/lib/generators/ocean_setup/templates/gitignore +37 -0
  28. data/lib/generators/ocean_setup/templates/hyperlinks.rb +22 -0
  29. data/lib/generators/ocean_setup/templates/ocean_constants.rb +36 -0
  30. data/lib/generators/ocean_setup/templates/routes.rb +8 -0
  31. data/lib/generators/ocean_setup/templates/spec_helper.rb +47 -0
  32. data/lib/generators/ocean_setup/templates/zeromq_logger.rb +15 -0
  33. data/lib/ocean-rails.rb +38 -0
  34. data/lib/ocean/api.rb +263 -0
  35. data/lib/ocean/api_resource.rb +135 -0
  36. data/lib/ocean/flooding.rb +29 -0
  37. data/lib/ocean/ocean_application_controller.rb +214 -0
  38. data/lib/ocean/ocean_resource_controller.rb +76 -0
  39. data/lib/ocean/ocean_resource_model.rb +61 -0
  40. data/lib/ocean/selective_rack_logger.rb +33 -0
  41. data/lib/ocean/version.rb +3 -0
  42. data/lib/ocean/zero_log.rb +184 -0
  43. data/lib/ocean/zeromq_logger.rb +42 -0
  44. data/lib/tasks/ocean_tasks.rake +4 -0
  45. data/lib/template.rb +31 -0
  46. data/lib/templates/rails/scaffold_controller/controller.rb +91 -0
  47. metadata +267 -0
@@ -0,0 +1,135 @@
1
+ module ApiResource
2
+
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+
8
+ module ClassMethods
9
+
10
+ #
11
+ # This method implements the common behaviour in Ocean for requesting collections
12
+ # of resources, including conditions, +GROUP+ and substring searches. It can be used
13
+ # directly on a class:
14
+ #
15
+ # @collection = ApiUser.collection(params)
16
+ #
17
+ # or on any Relation:
18
+ #
19
+ # @collection = @api_user.groups.collection(params)
20
+ #
21
+ # Since a Relation is returned, further chaining is possible:
22
+ #
23
+ # @collection = @api_user.groups.collection(params).active.order("email ASC")
24
+ #
25
+ # The whole params hash can safely be passed as the input arg: keys are filtered so
26
+ # that matches only are done against the attributes declared in the controller using
27
+ # +ocean_resource_model+.
28
+ #
29
+ # The +group:+ keyword arg, if present, adds a +GROUP+ clause to the generated SQL.
30
+ #
31
+ # The +search:+ keyword arg, if present, searches for the value in the database string or
32
+ # text column declared in the controller's +ocean_resource_model+ declaration.
33
+ # The search is done using an SQL +LIKE+ clause, with the substring framed by
34
+ # wildcard characters. It's self-evident that this is not an efficient search method
35
+ # for larger datasets; in such cases, other search methods should be employed.
36
+ #
37
+ # If +page:+ is present, pagination will be added. If +page+ is less than zero, an
38
+ # empty Relation will be returned. Otherwise, +page_size:+ (default 25) will be used
39
+ # to calculate OFFSET and LIMIT. The default +page_size+ for a resource class can
40
+ # also be declared using +ocean_resource_model+.
41
+ #
42
+ def collection(bag={})
43
+ collection_internal bag, bag[:group], bag[:search], bag[:page], bag[:page_size]
44
+ end
45
+
46
+
47
+ def collection_internal(conds={}, group, search, page, page_size)
48
+ if index_only != []
49
+ new_conds = {}
50
+ index_only.each { |key| new_conds[key] = conds[key] if conds[key].present? }
51
+ conds = new_conds
52
+ end
53
+ # Fold in the conditions
54
+ query = all.where(conds)
55
+ # Take care of grouping
56
+ query = query.group(group) if group.present? && index_only.include?(group.to_sym)
57
+ # Searching
58
+ if search.present?
59
+ return query.none if index_search_property.blank?
60
+ query = query.where("#{index_search_property} LIKE ?", "%#{search}%")
61
+ end
62
+ # Pagination
63
+ if page.present?
64
+ return query.none if page < 0
65
+ query = query.limit(page_size || collection_page_size).offset(page_size * page)
66
+ end
67
+ # Finally, return the accumulated Relation
68
+ query
69
+ end
70
+
71
+
72
+ #
73
+ # Returns the latest version for the resource class. E.g.:
74
+ #
75
+ # > ApiUser.latest_version
76
+ # "v1"
77
+ #
78
+ def latest_api_version
79
+ Api.version_for(self.class.name.pluralize.underscore)
80
+ end
81
+
82
+ #
83
+ # Invalidate all members of this class in Varnish using a +BAN+ requests to all
84
+ # caches in the Chef environment. The +BAN+ requests are done in parallel.
85
+ # The number of +BAN+ requests, and the exact +URI+ composition in each request,
86
+ # is determined by the +invalidate_collection:+ arg to the +ocean_resource_model+
87
+ # declaration in the model.
88
+ #
89
+ def invalidate
90
+ resource_name = name.pluralize.underscore
91
+ varnish_invalidate_collection.each do |suffix|
92
+ Api.ban "/v[0-9]+/#{resource_name}#{suffix}"
93
+ end
94
+ end
95
+
96
+ end
97
+
98
+
99
+ # Instance methods
100
+
101
+ #
102
+ # Convenience function used to touch two resources in one call, e.g:
103
+ #
104
+ # @api_user.touch_both(@connectee)
105
+ #
106
+ def touch_both(other)
107
+ touch
108
+ other.touch
109
+ end
110
+
111
+
112
+ #
113
+ # Invalidate the member and all its collections in Varnish using a +BAN+ requests to all
114
+ # caches in the Chef environment. The +BAN+ request are done in parallel.
115
+ # The number of +BAN+ requests, and the exact +URI+ composition in each request,
116
+ # is determined by the +invalidate_member:+ arg to the +ocean_resource_model+
117
+ # declaration in the model.
118
+ #
119
+ # The optional arg +avoid_self+, if true (the default is false), avoids invalidating
120
+ # the basic resource itself: only its derived collections are invalidated. This is useful
121
+ # when instantiating a new resource.
122
+ #
123
+ def invalidate(avoid_self=false)
124
+ self.class.invalidate
125
+ resource_name = self.class.name.pluralize.underscore
126
+ varnish_invalidate_member.each do |thing|
127
+ if thing.is_a?(String)
128
+ Api.ban "/v[0-9]+/#{resource_name}/#{self.id}#{thing}" if !avoid_self
129
+ else
130
+ Api.ban "/v[0-9]+/#{thing.call(self)}"
131
+ end
132
+ end
133
+ end
134
+
135
+ end
@@ -0,0 +1,29 @@
1
+ # # Rack::Attack.blacklist('block 1.2.3.4') do |req|
2
+ # # true
3
+ # # end
4
+
5
+ # Rack::Attack.blacklisted_response = lambda do |env|
6
+ # [ 403, {}, ['Blacklisted']]
7
+ # end
8
+
9
+
10
+ # Rack::Attack.throttle('req/ip', :limit => 1000, :period => 1.second) do |req|
11
+ # # If the return value is truthy, the cache key for the return value
12
+ # # is incremented and compared with the limit. In this case:
13
+ # # "rack::attack:#{Time.now.to_i/1.second}:req/ip:#{req.ip}"
14
+ # # We might want to use the token value instead of the #{req.ip} value.
15
+ # # (IPs may be shared, tokens never are.)
16
+ # #
17
+ # # If falsy, the cache key is neither incremented nor checked.
18
+ # req.ip
19
+ # end
20
+
21
+ # Rack::Attack.throttled_response = lambda do |env|
22
+ # allowed = env['rack.attack.match_data'][:limit]
23
+ # t = env['rack.attack.match_data'][:period].inspect
24
+ # made = env['rack.attack.match_data'][:count]
25
+ # retry_after = env['rack.attack.match_data'][:period] rescue nil
26
+ # [ 429,
27
+ # {'Retry-After' => retry_after.to_s},
28
+ # ["Too Many Requests. You have exceeded your quota of #{allowed} request(s)/#{t} by #{made - allowed}."]]
29
+ # end
@@ -0,0 +1,214 @@
1
+ module OceanApplicationController
2
+
3
+ #
4
+ # Sets the default URL generation options to the HTTPS protocol, and
5
+ # the host to the OCEAN_API_HOST, that is, to the external URL of the
6
+ # Ocean API. We always generate external URIs, even for internal calls.
7
+ # It's the responsibility of the other service to rewrite external
8
+ # to internal URIs when calling the internal API point.
9
+ #
10
+ def default_url_options(options = nil)
11
+ { :protocol => "https", :host => OCEAN_API_HOST }
12
+ end
13
+
14
+
15
+ #
16
+ # Ensures that there is an +X-API-Token+ HTTP header in the request.
17
+ # Stores the token in @x_api_token for use during authorisation of the
18
+ # current controller action. If there's no +X-API-Token+ header, the
19
+ # request is aborted and an API error with status 400 is returned.
20
+ #
21
+ # 400 error responses will always contain a body with error information
22
+ # explaining the API error:
23
+ #
24
+ # {"_api_error": ["X-API-Token missing"]}
25
+ #
26
+ # or
27
+ #
28
+ # {"_api_error": ["Authentication expired"]}
29
+ #
30
+ def require_x_api_token
31
+ return true if ENV['NO_OCEAN_AUTH']
32
+ @x_api_token = request.headers['X-API-Token']
33
+ return true if @x_api_token.present?
34
+ logger.info "X-API-Token missing"
35
+ render_api_error 400, "X-API-Token missing"
36
+ false
37
+ end
38
+
39
+
40
+
41
+ #
42
+ # Class variable to hold any extra controller actions defined in the
43
+ # +ocean_resource_controller+ declaration in the resource controller.
44
+ #
45
+ @@extra_actions = {}
46
+
47
+
48
+ #
49
+ # Performs authorisation of the current action. Returns true if allowed,
50
+ # false if not. Calls the Auth service using a +GET+, which means previous
51
+ # authorisations using the same token and args will be cached in Varnish.
52
+ #
53
+ def authorize_action
54
+ return true if ENV['NO_OCEAN_AUTH']
55
+ # Obtain any nonstandard actions
56
+ @@extra_actions[controller_name] ||= begin
57
+ extra_actions
58
+ rescue NameError => e
59
+ {}
60
+ end
61
+ # Create a query string and call Auth
62
+ qs = Api.authorization_string(@@extra_actions, controller_name, action_name)
63
+ response = Api.permitted?(@x_api_token, query: qs)
64
+ if response.status == 200
65
+ @auth_api_user_id = response.body['authentication']['user_id'] # Deprecate and remove
66
+ @auth_api_user_uri = response.body['authentication']['_links']['creator']['href'] # Keep
67
+ return true
68
+ end
69
+ error_messages = response.body['_api_error']
70
+ render_api_error response.status, *error_messages
71
+ false
72
+ end
73
+
74
+
75
+ #
76
+ # Updates +created_by+ and +updated_by+ to the ApiUser for which the current request
77
+ # is authorised. The attributes can be declared either String (recommended) or
78
+ # Integer (deprecated). If String, they will be set to the URI of the ApiUser. (If
79
+ # Integer, to their internal SQL ID.)
80
+ #
81
+ def set_updater(obj)
82
+ id_or_uri = obj.created_by.is_a?(Integer) ? @auth_api_user_id : @auth_api_user_uri
83
+ obj.created_by = id_or_uri if obj.created_by.blank? || obj.created_by == 0
84
+ obj.updated_by = id_or_uri
85
+ end
86
+
87
+
88
+ #
89
+ # Renders an API level error. The body will be a JSON hash with a single key,
90
+ # +_api_error+. The value is an array containing the +messages+.
91
+ #
92
+ # render_api_error(500, "An unforeseen error occurred")
93
+ #
94
+ # results in a response with HTTP status 500 and the following body:
95
+ #
96
+ # {"_api_error": ["An unforeseen error occurred"]}
97
+ #
98
+ # Resource consumers should always examine the body when an error is returned,
99
+ # as +_api_error+ always will give additional information which may be required
100
+ # to process the error properly.
101
+ #
102
+ def render_api_error(status_code, *messages)
103
+ render json: {_api_error: messages}, status: status_code
104
+ end
105
+
106
+ #
107
+ # Renders a +HEAD+ response with HTTP status 204 No Content.
108
+ #
109
+ def render_head_204
110
+ render text: '', status: 204, content_type: 'application/json'
111
+ end
112
+
113
+ #
114
+ # Renders a HTTP 422 Unprocessable Entity response with a body enumerating
115
+ # each invalid Rails resource attribute and all their errors. This is usually
116
+ # done in response to detecting a resource is invalid during +POST+ (create) and
117
+ # +PUT/PATCH+ (update). E.g.:
118
+ #
119
+ # {"name": ["must be specified"],
120
+ # "email": ["must be specified", "must contain a @ character"]}
121
+ #
122
+ # The messages are intended for presentation to an end user.
123
+ #
124
+ def render_validation_errors(r)
125
+ render json: r.errors, :status => 422
126
+ end
127
+
128
+ #
129
+ # This is the main rendering function in Ocean. The argument +x+ can be a resource
130
+ # or a collection of resources (which need not be of the same type).
131
+ #
132
+ # The keyword arg +new+, if true, sets the response HTTP status to 201 and also adds
133
+ # a +Location+ HTTP header with the URI of the resource.
134
+ #
135
+ # Rendering is done using partials only. These should by convention be located in
136
+ # their standard position, begin with an underscore, etc. The +ocean+ gem generator
137
+ # for resources creates a partial in the proper location.
138
+ #
139
+ def api_render(x, new: false)
140
+ if !x.is_a?(Array) && !x.is_a?(ActiveRecord::Relation)
141
+ partial = x.to_partial_path
142
+ if new
143
+ render partial: partial, object: x, status: 201, location: x
144
+ else
145
+ render partial: partial, object: x
146
+ end
147
+ return
148
+ elsif x == []
149
+ render text: '[]'
150
+ return
151
+ else
152
+ partials = x.collect { |m| render_to_string(partial: m.to_partial_path,
153
+ locals: {m.class.model_name.i18n_key => m}) }
154
+ render text: '[' + partials.join(',') + ']'
155
+ end
156
+ end
157
+
158
+
159
+ #
160
+ # Filters away all non-accessible attributes from params. Thus, we still are
161
+ # using pre-Rails 4.0 protected attributes. This will eventually be replaced
162
+ # by strong parameters. Takes a class and returns a new hash containing only
163
+ # the model attributes which may be modified.
164
+ #
165
+ def filtered_params(klass)
166
+ result = {}
167
+ params.each do |k, v|
168
+ result[k] = v if klass.accessible_attributes.include?(k)
169
+ end
170
+ result
171
+ end
172
+
173
+
174
+ #
175
+ # Cache values for collections. Accepts a class or a scope. The cache value
176
+ # is based on three components: (1) the name of the class, (2) the number of
177
+ # members in the collection, and (3) the modification time of the last updated
178
+ # member.
179
+ #
180
+ def collection_etag(coll)
181
+ coll.name.constantize # Force a load of the class (for secondary collections)
182
+ last_updated = coll.order(:updated_at).last.updated_at.utc rescue 0
183
+ # We could also, in the absence of an updated_at attribute, use created_at.
184
+ { etag: "#{coll.name}:#{coll.count}:#{last_updated}"
185
+ }
186
+ end
187
+
188
+
189
+ #
190
+ # This method finds the other resource for connect/disconnect, given the
191
+ # value of the param +href+, which should be a complete resource URI.
192
+ #
193
+ # Renders API errors if the +href+ arg is missing, can't be parsed, or
194
+ # the resource can't be found.
195
+ #
196
+ # Sets @connectee_class to the class of the resource pointed to by +href+,
197
+ # and @connectee to the resource itself.
198
+ #
199
+ def find_connectee
200
+ href = params[:href]
201
+ render_api_error(422, "href query arg is missing") and return if href.blank?
202
+ begin
203
+ routing = Rails.application.routes.recognize_path(href)
204
+ rescue ActionController::RoutingError
205
+ render_api_error(422, "href query arg isn't parseable")
206
+ return
207
+ end
208
+ @connectee_class = routing[:controller].classify.constantize
209
+ @connectee = @connectee_class.find_by_id(routing[:id])
210
+ render_api_error(404, "Resource to connect not found") and return unless @connectee
211
+ true
212
+ end
213
+
214
+ end
@@ -0,0 +1,76 @@
1
+ #
2
+ # This is an "acts_as" type method to be used in ActiveRecord model
3
+ # definitions: "ocean_resource_controller".
4
+ #
5
+
6
+ module Ocean
7
+ module OceanResourceController
8
+
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ end
13
+
14
+ module ClassMethods
15
+
16
+ #
17
+ # The presence of +ocean_resource_controller+ in a Rails controller declares
18
+ # that the controller is an Ocean controller handling an Ocean resource. It takes
19
+ # two keyword parameters:
20
+ #
21
+ # +required_attributes+: a list of keywords naming model attributes which must be
22
+ # present in every update operation. If an API consumer submits data where any
23
+ # of these attributes isn't present, an API error will be generated.
24
+ #
25
+ # ocean_resource_controller required_attributes: [:lock_version, :title]
26
+ #
27
+ # +extra_actions+: a hash containing information about extra controller actions
28
+ # apart from the standard Rails ones of +index+, +show+, +create+, +update+, and
29
+ # +destroy+. One entry per extra action is required in order to process authentication
30
+ # requests. Here's an example:
31
+ #
32
+ # ocean_resource_controller extra_actions: {'comments' => ['comments', "GET"],
33
+ # 'comment_create' => ['comments', "POST"]}
34
+ #
35
+ # The above example declares that the controller has two non-standard actions called
36
+ # +comments+ and +comments_create+, respectively. Their respective values indicate that
37
+ # +comments+ will be called as the result of a +GET+ to the +comments+ hyperlink, and
38
+ # that +comment_create+ will be called as the result of a +POST+ to the same hyperlink.
39
+ # Thus, +extra_actions+ maps actions to hyperlink names and HTTP methods.
40
+ #
41
+ def ocean_resource_controller(required_attributes: [:lock_version, :name, :description],
42
+ extra_actions: {}
43
+ )
44
+ cattr_accessor :ocean_resource_controller_extra_actions
45
+ cattr_accessor :ocean_resource_controller_required_attributes
46
+ self.ocean_resource_controller_extra_actions = extra_actions
47
+ self.ocean_resource_controller_required_attributes = required_attributes
48
+ end
49
+ end
50
+
51
+
52
+ #
53
+ # Used in controller code internals to obtain the extra actions declared using
54
+ # +ocean_resource_controller+.
55
+ #
56
+ def extra_actions
57
+ self.class.ocean_resource_controller_extra_actions
58
+ end
59
+
60
+
61
+ #
62
+ # Returns true if the params hash lacks a required attribute declared using
63
+ # +ocean_resource_controller+.
64
+ #
65
+ def missing_attributes?
66
+ self.class.ocean_resource_controller_required_attributes.each do |attr|
67
+ return true unless params[attr]
68
+ end
69
+ return false
70
+ end
71
+
72
+ end
73
+ end
74
+
75
+
76
+ ActionController::Base.send :include, Ocean::OceanResourceController