apicasso 0.6.4 → 0.6.5
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.
- checksums.yaml +4 -4
- data/README.md +6 -2
- data/app/controllers/apicasso/application_controller.rb +4 -82
- data/app/controllers/apicasso/crud_controller.rb +1 -45
- data/app/controllers/concerns/crud_utils.rb +139 -0
- data/app/controllers/concerns/sql_security.rb +5 -4
- data/lib/apicasso/version.rb +5 -1
- data/spec/dummy/log/test.log +2291 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d09d0e339bfb17aa767876e5041f749e5154cedb2c327588713a14113d0e00dd
|
4
|
+
data.tar.gz: dd03efe8e6a7a64e4f67cae8e19f2d8be5a0782f10d1f0524114f3071969bbca
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a456ef2a8afb8c238f99b693cbd3bf650753fb6c07f6c5e31724cd03e7d5607cb6edfbc67004521e1abcccbf13f80b628ba197fef4876993cfd1c8a4a6f36aae
|
7
|
+
data.tar.gz: 718d3734733cbeee045546495cd5f26985c799257172ca0434df0a7bd7db64e8a165c537ac17d1fda4f0cf8e3f82343bc9f8aa852e3d70958ffe4e4905ab77da
|
data/README.md
CHANGED
@@ -11,7 +11,7 @@ You can make your own API with only 4 steps:
|
|
11
11
|
### Step 1
|
12
12
|
Create your models
|
13
13
|
### Step 2
|
14
|
-
Insert **APIcasso** engine into your routes
|
14
|
+
Insert **APIcasso** engine into your routes and run the installation command
|
15
15
|
### Step 3
|
16
16
|
[Create an Apicasso::Key](https://github.com/autoforce/APIcasso#authorization)
|
17
17
|
### Step 4
|
@@ -36,6 +36,7 @@ $ bundle install && rails g apicasso:install
|
|
36
36
|
|
37
37
|
- PostgreSQL with JSON columns support
|
38
38
|
- Ruby 2.3+
|
39
|
+
- Rails 5+
|
39
40
|
|
40
41
|
# Usage
|
41
42
|
|
@@ -90,6 +91,10 @@ And in your `app/controllers/custom_controller.rb` you would have something like
|
|
90
91
|
|
91
92
|
This way you enjoy all our object finder, authorization and authentication features, making your job more straight into your business logic.
|
92
93
|
|
94
|
+
## CORS
|
95
|
+
|
96
|
+
APIcasso comes with a permissive CORS configuration out of the box. But you can make your own by editting the `config/initializers/apicasso.rb` file, which is created at the installation proccess. The file comes with some descriptive comments and all configuration is based on [Rack CORS](https://github.com/cyu/rack-cors) options.
|
97
|
+
|
93
98
|
## Authentication
|
94
99
|
|
95
100
|
> But exposing my models to the internet is permissive as hell! Haven't you thought about security?
|
@@ -223,7 +228,6 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/Ervalh
|
|
223
228
|
### TODO
|
224
229
|
|
225
230
|
- Add support to other databases
|
226
|
-
- [Abstract a configurable CORS approach, maybe using middleware](https://github.com/autoforce/APIcasso/issues/22)
|
227
231
|
- Add gem options like: Token rotation, Alternative authentication methods
|
228
232
|
- Refine and document auto-documentation feature
|
229
233
|
- Rate limiting
|
@@ -60,91 +60,13 @@ module Apicasso
|
|
60
60
|
def response_metadata
|
61
61
|
{
|
62
62
|
status: response.status,
|
63
|
-
body:
|
63
|
+
body: parsed_response
|
64
64
|
}
|
65
65
|
end
|
66
66
|
|
67
|
-
#
|
68
|
-
def
|
69
|
-
(
|
70
|
-
end
|
71
|
-
|
72
|
-
# A method to extract all assosciations available
|
73
|
-
def associations_array
|
74
|
-
resource.reflect_on_all_associations.map { |association| association.name.to_s }
|
75
|
-
end
|
76
|
-
|
77
|
-
# Used to avoid errors parsing the search query, which can be passed as
|
78
|
-
# a JSON or as a key-value param. JSON is preferred because it generates
|
79
|
-
# shorter URLs on GET parameters.
|
80
|
-
def parsed_query
|
81
|
-
JSON.parse(params[:q])
|
82
|
-
rescue JSON::ParserError, TypeError
|
83
|
-
params[:q]
|
84
|
-
end
|
85
|
-
|
86
|
-
# Used to avoid errors in included associations parsing and to enable a
|
87
|
-
# insertion point for a change on splitting method.
|
88
|
-
def parsed_associations
|
89
|
-
params[:include].split(',').map do |param|
|
90
|
-
if @object.respond_to?(param)
|
91
|
-
param if associations_array.include?(param)
|
92
|
-
end
|
93
|
-
end.compact
|
94
|
-
rescue NoMethodError
|
95
|
-
[]
|
96
|
-
end
|
97
|
-
|
98
|
-
# Used to avoid errors in included associations parsing and to enable a
|
99
|
-
# insertion point for a change on splitting method.
|
100
|
-
def parsed_methods
|
101
|
-
params[:include].split(',').map do |param|
|
102
|
-
if @object.respond_to?(param)
|
103
|
-
param unless associations_array.include?(param)
|
104
|
-
end
|
105
|
-
end.compact
|
106
|
-
rescue NoMethodError
|
107
|
-
[]
|
108
|
-
end
|
109
|
-
|
110
|
-
# Used to avoid errors in fieldset selection parsing and to enable a
|
111
|
-
# insertion point for a change on splitting method.
|
112
|
-
def parsed_select
|
113
|
-
params[:select].split(',').map do |field|
|
114
|
-
field if resource.column_names.include?(field)
|
115
|
-
end
|
116
|
-
rescue NoMethodError
|
117
|
-
[]
|
118
|
-
end
|
119
|
-
|
120
|
-
# Receives a `.paginate`d collection and returns the pagination
|
121
|
-
# metadata to be merged into response
|
122
|
-
def pagination_metadata_for(records)
|
123
|
-
{ total: records.total_entries,
|
124
|
-
total_pages: records.total_pages,
|
125
|
-
last_page: records.next_page.blank?,
|
126
|
-
previous_page: previous_link_for(records),
|
127
|
-
next_page: next_link_for(records),
|
128
|
-
out_of_bounds: records.out_of_bounds?,
|
129
|
-
offset: records.offset }
|
130
|
-
end
|
131
|
-
|
132
|
-
# Generates a contextualized URL of the next page for the request
|
133
|
-
def next_link_for(records)
|
134
|
-
uri = URI.parse(request.original_url)
|
135
|
-
query = Rack::Utils.parse_query(uri.query)
|
136
|
-
query['page'] = records.next_page
|
137
|
-
uri.query = Rack::Utils.build_query(query)
|
138
|
-
uri.to_s
|
139
|
-
end
|
140
|
-
|
141
|
-
# Generates a contextualized URL of the previous page for the request
|
142
|
-
def previous_link_for(records)
|
143
|
-
uri = URI.parse(request.original_url)
|
144
|
-
query = Rack::Utils.parse_query(uri.query)
|
145
|
-
query['page'] = records.previous_page
|
146
|
-
uri.query = Rack::Utils.build_query(query)
|
147
|
-
uri.to_s
|
67
|
+
# Parsed response to save as metadata for the requests
|
68
|
+
def parsed_response
|
69
|
+
(response.body.present? ? JSON.parse(response.body) : '')
|
148
70
|
end
|
149
71
|
|
150
72
|
# Receives a `:action, :resource, :object` hash to validate authorization
|
@@ -8,6 +8,7 @@ module Apicasso
|
|
8
8
|
before_action :set_nested_resource, only: %i[nested_index]
|
9
9
|
before_action :set_records, only: %i[index nested_index]
|
10
10
|
include SqlSecurity
|
11
|
+
include CrudUtils
|
11
12
|
include Orderable
|
12
13
|
# GET /:resource
|
13
14
|
# Returns a paginated, ordered and filtered query based response.
|
@@ -90,10 +91,6 @@ module Apicasso
|
|
90
91
|
authorize! action_to_cancancan, @object
|
91
92
|
end
|
92
93
|
|
93
|
-
def action_to_cancancan
|
94
|
-
action_name == 'nested_index' ? :index : action_name.to_sym
|
95
|
-
end
|
96
|
-
|
97
94
|
# Used to setup the resource's schema, mapping attributes and it's types
|
98
95
|
def resource_schema
|
99
96
|
schemated = {}
|
@@ -169,12 +166,6 @@ module Apicasso
|
|
169
166
|
total: @records.size }
|
170
167
|
end
|
171
168
|
|
172
|
-
# Parse to include options
|
173
|
-
def include_options
|
174
|
-
{ include: parsed_associations || [],
|
175
|
-
methods: parsed_methods || [] }
|
176
|
-
end
|
177
|
-
|
178
169
|
# Parsed JSON to be used as response payload, with included relations
|
179
170
|
def include_relations
|
180
171
|
@records = @records.includes(parsed_associations)
|
@@ -196,41 +187,6 @@ module Apicasso
|
|
196
187
|
.permit(resource_params)
|
197
188
|
end
|
198
189
|
|
199
|
-
# Resource params mapping, with a twist:
|
200
|
-
# Including relations as they are needed
|
201
|
-
def resource_params
|
202
|
-
built = resource_schema.keys
|
203
|
-
built += has_one_params if has_one_params.present?
|
204
|
-
built += has_many_params if has_many_params.present?
|
205
|
-
built
|
206
|
-
end
|
207
|
-
|
208
|
-
# A wrapper to has_one relations parameter building
|
209
|
-
def has_one_params
|
210
|
-
resource.reflect_on_all_associations(:has_one).map do |one|
|
211
|
-
if one.class_name.starts_with?('ActiveStorage')
|
212
|
-
next if one.class_name.ends_with?('Blob')
|
213
|
-
|
214
|
-
one.name.to_s.gsub(/(_attachment)$/, '').to_sym
|
215
|
-
else
|
216
|
-
one.name
|
217
|
-
end
|
218
|
-
end.compact
|
219
|
-
end
|
220
|
-
|
221
|
-
# A wrapper to has_many parameter building
|
222
|
-
def has_many_params
|
223
|
-
resource.reflect_on_all_associations(:has_many).map do |many|
|
224
|
-
if many.class_name.starts_with?('ActiveStorage')
|
225
|
-
next if many.class_name.ends_with?('Blob')
|
226
|
-
|
227
|
-
{ many.name.to_s.gsub(/(_attachments)$/, '').to_sym => [] }
|
228
|
-
else
|
229
|
-
{ many.name.to_sym => [] }
|
230
|
-
end
|
231
|
-
end.compact
|
232
|
-
end
|
233
|
-
|
234
190
|
# Common setup to stablish which model is the resource of this request
|
235
191
|
def set_root_resource
|
236
192
|
@root_resource = params[:resource].classify.constantize
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Module to extract utilities used on CRUD controllers.
|
4
|
+
# It makes it easier to parse parameters, proccess requests
|
5
|
+
# and build rich responses.
|
6
|
+
module CrudUtils
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
# Reutrns root_resource if nested_resource is not set scoped by permissions
|
10
|
+
def resource
|
11
|
+
(@nested_resource || @root_resource)
|
12
|
+
end
|
13
|
+
|
14
|
+
# A method to extract all assosciations available
|
15
|
+
def associations_array
|
16
|
+
resource.reflect_on_all_associations.map { |association| association.name.to_s }
|
17
|
+
end
|
18
|
+
|
19
|
+
# An parser to the action name so that nested_index has the same
|
20
|
+
# authorization behavior as index
|
21
|
+
def action_to_cancancan
|
22
|
+
action_name == 'nested_index' ? :index : action_name.to_sym
|
23
|
+
end
|
24
|
+
|
25
|
+
# Resource params mapping, with a twist:
|
26
|
+
# Including relations as they are needed
|
27
|
+
def resource_params
|
28
|
+
built = resource_schema.keys
|
29
|
+
built += has_one_params if has_one_params.present?
|
30
|
+
built += has_many_params if has_many_params.present?
|
31
|
+
built
|
32
|
+
end
|
33
|
+
|
34
|
+
# A wrapper to has_one relations parameter building
|
35
|
+
def has_one_params
|
36
|
+
resource.reflect_on_all_associations(:has_one).map do |one|
|
37
|
+
if one.class_name.starts_with?('ActiveStorage')
|
38
|
+
next if one.class_name.ends_with?('Blob')
|
39
|
+
|
40
|
+
one.name.to_s.gsub(/(_attachment)$/, '').to_sym
|
41
|
+
else
|
42
|
+
one.name
|
43
|
+
end
|
44
|
+
end.compact
|
45
|
+
end
|
46
|
+
|
47
|
+
# A wrapper to has_many parameter building
|
48
|
+
def has_many_params
|
49
|
+
resource.reflect_on_all_associations(:has_many).map do |many|
|
50
|
+
if many.class_name.starts_with?('ActiveStorage')
|
51
|
+
next if many.class_name.ends_with?('Blob')
|
52
|
+
|
53
|
+
{ many.name.to_s.gsub(/(_attachments)$/, '').to_sym => [] }
|
54
|
+
else
|
55
|
+
{ many.name.to_sym => [] }
|
56
|
+
end
|
57
|
+
end.compact
|
58
|
+
end
|
59
|
+
|
60
|
+
# Parse to include options
|
61
|
+
def include_options
|
62
|
+
{ include: parsed_associations || [],
|
63
|
+
methods: parsed_methods || [] }
|
64
|
+
end
|
65
|
+
|
66
|
+
# Used to avoid errors parsing the search query, which can be passed as
|
67
|
+
# a JSON or as a key-value param. JSON is preferred because it generates
|
68
|
+
# shorter URLs on GET parameters.
|
69
|
+
def parsed_query
|
70
|
+
JSON.parse(params[:q])
|
71
|
+
rescue JSON::ParserError, TypeError
|
72
|
+
params[:q]
|
73
|
+
end
|
74
|
+
|
75
|
+
# Used to avoid errors in included associations parsing and to enable a
|
76
|
+
# insertion point for a change on splitting method.
|
77
|
+
def parsed_associations
|
78
|
+
params[:include].split(',').map do |param|
|
79
|
+
if @object.respond_to?(param)
|
80
|
+
param if associations_array.include?(param)
|
81
|
+
end
|
82
|
+
end.compact
|
83
|
+
rescue NoMethodError
|
84
|
+
[]
|
85
|
+
end
|
86
|
+
|
87
|
+
# Used to avoid errors in included associations parsing and to enable a
|
88
|
+
# insertion point for a change on splitting method.
|
89
|
+
def parsed_methods
|
90
|
+
params[:include].split(',').map do |param|
|
91
|
+
if @object.respond_to?(param)
|
92
|
+
param unless associations_array.include?(param)
|
93
|
+
end
|
94
|
+
end.compact
|
95
|
+
rescue NoMethodError
|
96
|
+
[]
|
97
|
+
end
|
98
|
+
|
99
|
+
# Used to avoid errors in fieldset selection parsing and to enable a
|
100
|
+
# insertion point for a change on splitting method.
|
101
|
+
def parsed_select
|
102
|
+
params[:select].split(',').map do |field|
|
103
|
+
field if resource.column_names.include?(field)
|
104
|
+
end
|
105
|
+
rescue NoMethodError
|
106
|
+
[]
|
107
|
+
end
|
108
|
+
|
109
|
+
# Receives a `.paginate`d collection and returns the pagination
|
110
|
+
# metadata to be merged into response
|
111
|
+
def pagination_metadata_for(records)
|
112
|
+
{ total: records.total_entries,
|
113
|
+
total_pages: records.total_pages,
|
114
|
+
last_page: records.next_page.blank?,
|
115
|
+
previous_page: previous_link_for(records),
|
116
|
+
next_page: next_link_for(records),
|
117
|
+
out_of_bounds: records.out_of_bounds?,
|
118
|
+
offset: records.offset }
|
119
|
+
end
|
120
|
+
|
121
|
+
# Generates a contextualized URL of the next page for the request
|
122
|
+
def next_link_for(records)
|
123
|
+
page_link(records, page: 'next')
|
124
|
+
end
|
125
|
+
|
126
|
+
# Generates a contextualized URL of the previous page for the request
|
127
|
+
def previous_link_for(records)
|
128
|
+
page_link(records, page: 'previous')
|
129
|
+
end
|
130
|
+
|
131
|
+
# Common pagination link generation helper
|
132
|
+
def page_link(records, opts = {})
|
133
|
+
uri = URI.parse(request.original_url)
|
134
|
+
query = Rack::Utils.parse_query(uri.query)
|
135
|
+
query['page'] = records.send("#{opts[:page]}_page")
|
136
|
+
uri.query = Rack::Utils.build_query(query)
|
137
|
+
uri.to_s
|
138
|
+
end
|
139
|
+
end
|
@@ -46,12 +46,13 @@ module SqlSecurity
|
|
46
46
|
|
47
47
|
# Check for a bad request to be more secure
|
48
48
|
def klasses_allowed
|
49
|
-
raise ActionController::BadRequest.new('Bad hacker, stop be bully or I will tell to your mom!') unless
|
49
|
+
raise ActionController::BadRequest.new('Bad hacker, stop be bully or I will tell to your mom!') unless safe_resource?
|
50
50
|
end
|
51
51
|
|
52
|
-
# Check if it's
|
53
|
-
def
|
54
|
-
|
52
|
+
# Check if it's safe to use the requet
|
53
|
+
def safe_resource?
|
54
|
+
controller_name == representative_resource ||
|
55
|
+
DESCENDANTS_UNDERSCORED.include?(param_attribute.to_s.underscore)
|
55
56
|
end
|
56
57
|
|
57
58
|
# Get param to be compared
|