ocean-rails 1.14.0

Sign up to get free protection for your applications and to get access to all the features.
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