apicasso 0.4.11 → 0.5.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.
- checksums.yaml +4 -4
- data/README.md +1 -3
- data/Rakefile +0 -0
- data/app/controllers/apicasso/apidocs_controller.rb +332 -326
- data/app/controllers/apicasso/application_controller.rb +46 -1
- data/app/controllers/apicasso/crud_controller.rb +4 -20
- data/app/controllers/concerns/orderable.rb +1 -1
- data/app/controllers/concerns/sql_security.rb +67 -0
- data/app/models/apicasso/ability.rb +3 -0
- data/app/models/apicasso/application_record.rb +0 -0
- data/app/models/apicasso/key.rb +0 -0
- data/app/models/apicasso/request.rb +0 -0
- data/config/routes.rb +7 -0
- data/lib/apicasso/active_record_extension.rb +5 -0
- data/lib/apicasso/engine.rb +0 -0
- data/lib/apicasso/version.rb +1 -1
- data/lib/apicasso.rb +0 -0
- data/lib/generators/apicasso/install/install_generator.rb +6 -0
- data/lib/generators/apicasso/install/templates/create_apicasso_tables.rb +8 -0
- data/spec/apicasso_spec.rb +0 -0
- data/spec/dummy/Gemfile +0 -0
- data/spec/dummy/Gemfile.lock +0 -0
- data/spec/dummy/Rakefile +0 -0
- data/spec/dummy/app/controllers/application_controller.rb +0 -0
- data/spec/dummy/app/models/application_record.rb +0 -0
- data/spec/dummy/app/models/used_model.rb +0 -0
- data/spec/dummy/bin/bundle +0 -0
- data/spec/dummy/bin/rails +0 -0
- data/spec/dummy/bin/rake +0 -0
- data/spec/dummy/bin/setup +0 -0
- data/spec/dummy/bin/spring +0 -0
- data/spec/dummy/bin/update +0 -0
- data/spec/dummy/config/application.rb +0 -0
- data/spec/dummy/config/boot.rb +0 -0
- data/spec/dummy/config/cable.yml +0 -0
- data/spec/dummy/config/credentials.yml.enc +0 -0
- data/spec/dummy/config/database.yml +0 -0
- data/spec/dummy/config/environment.rb +0 -0
- data/spec/dummy/config/environments/development.rb +0 -0
- data/spec/dummy/config/environments/production.rb +0 -0
- data/spec/dummy/config/environments/test.rb +0 -0
- data/spec/dummy/config/initializers/application_controller_renderer.rb +0 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +0 -0
- data/spec/dummy/config/initializers/cors.rb +0 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +0 -0
- data/spec/dummy/config/initializers/inflections.rb +0 -0
- data/spec/dummy/config/initializers/mime_types.rb +0 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +0 -0
- data/spec/dummy/config/locales/en.yml +0 -0
- data/spec/dummy/config/puma.rb +0 -0
- data/spec/dummy/config/routes.rb +0 -0
- data/spec/dummy/config/spring.rb +0 -0
- data/spec/dummy/config/storage.yml +0 -0
- data/spec/dummy/config.ru +0 -0
- data/spec/dummy/db/migrate/20180918134607_create_apicasso_tables.rb +0 -0
- data/spec/dummy/db/migrate/20180918141254_create_used_models.rb +0 -0
- data/spec/dummy/db/migrate/20180919130152_create_active_storage_tables.active_storage.rb +0 -0
- data/spec/dummy/db/migrate/20180920133933_change_used_model_to_validates.rb +0 -0
- data/spec/dummy/db/schema.rb +0 -0
- data/spec/dummy/db/seeds.rb +0 -0
- data/spec/dummy/package.json +0 -0
- data/spec/factories/used_model.rb +0 -0
- data/spec/models/used_model_spec.rb +0 -0
- data/spec/rails_helper.rb +0 -0
- data/spec/requests/bad_requests_spec.rb +51 -0
- data/spec/requests/requests_spec.rb +98 -23
- data/spec/spec_helper.rb +1 -1
- data/spec/support/database_cleaner.rb +8 -0
- data/spec/support/factory_bot.rb +0 -0
- data/spec/token/token_spec.rb +322 -0
- metadata +32 -27
- data/spec/dummy/app/serializers/used_model_serializer.rb +0 -3
@@ -7,9 +7,14 @@ module Apicasso
|
|
7
7
|
class ApplicationController < ActionController::API
|
8
8
|
include ActionController::HttpAuthentication::Token::ControllerMethods
|
9
9
|
prepend_before_action :restrict_access, unless: -> { preflight? }
|
10
|
+
prepend_before_action :klasses_allowed
|
10
11
|
before_action :set_access_control_headers
|
12
|
+
before_action :set_root_resource
|
13
|
+
before_action :bad_request?
|
11
14
|
after_action :register_api_request
|
12
15
|
|
16
|
+
include SqlSecurity
|
17
|
+
|
13
18
|
# Sets the authorization scope for the current API key, it's a getter
|
14
19
|
# to make scoping easier
|
15
20
|
def current_ability
|
@@ -65,6 +70,21 @@ module Apicasso
|
|
65
70
|
}
|
66
71
|
end
|
67
72
|
|
73
|
+
# Common setup to stablish which model is the resource of this request
|
74
|
+
def set_root_resource
|
75
|
+
@root_resource = params[:resource].classify.constantize
|
76
|
+
end
|
77
|
+
|
78
|
+
# Setup to stablish the nested model to be queried
|
79
|
+
def set_nested_resource
|
80
|
+
@nested_resource = @object.send(params[:nested].underscore.pluralize)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Reutrns root_resource if nested_resource is not set scoped by permissions
|
84
|
+
def resource
|
85
|
+
(@nested_resource || @root_resource)
|
86
|
+
end
|
87
|
+
|
68
88
|
# A method to extract all assosciations available
|
69
89
|
def associations_array
|
70
90
|
resource.reflect_on_all_associations.map { |association| association.name.to_s }
|
@@ -107,7 +127,7 @@ module Apicasso
|
|
107
127
|
# insertion point for a change on splitting method.
|
108
128
|
def parsed_select
|
109
129
|
params[:select].split(',').map do |field|
|
110
|
-
field if
|
130
|
+
field if resource.column_names.include?(field)
|
111
131
|
end
|
112
132
|
rescue NoMethodError
|
113
133
|
[]
|
@@ -143,6 +163,25 @@ module Apicasso
|
|
143
163
|
uri.to_s
|
144
164
|
end
|
145
165
|
|
166
|
+
# Check for a bad request to be more secure
|
167
|
+
def klasses_allowed
|
168
|
+
raise ActionController::BadRequest.new('Bad hacker, stop be bully or I will tell to your mom!') unless descendants_included?
|
169
|
+
end
|
170
|
+
|
171
|
+
# Check if it's a descendant model allowed
|
172
|
+
def descendants_included?
|
173
|
+
DESCENDANTS_UNDERSCORED.include?(param_attribute.to_s.underscore)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Get param to be compared
|
177
|
+
def param_attribute
|
178
|
+
representative_resource.singularize
|
179
|
+
end
|
180
|
+
|
181
|
+
def representative_resource
|
182
|
+
(params[:nested] || params[:resource] || controller_name)
|
183
|
+
end
|
184
|
+
|
146
185
|
# Receives a `:action, :resource, :object` hash to validate authorization
|
147
186
|
# Example:
|
148
187
|
# > authorize_for action: :read, resource: :object_class, object: :object
|
@@ -151,6 +190,12 @@ module Apicasso
|
|
151
190
|
authorize! opts[:action], opts[:object] if opts[:object].present?
|
152
191
|
end
|
153
192
|
|
193
|
+
# Check for SQL injection before requests and
|
194
|
+
# raise a exception when find
|
195
|
+
def bad_request?
|
196
|
+
raise ActionController::BadRequest.new('Bad hacker, stop be bully or I will tell to your mom!') unless sql_injection(resource)
|
197
|
+
end
|
198
|
+
|
154
199
|
# @TODO
|
155
200
|
# Remove this in favor of a more controllable aproach of CORS
|
156
201
|
def set_access_control_headers
|
@@ -3,10 +3,9 @@
|
|
3
3
|
module Apicasso
|
4
4
|
# Controller to consume read-only data to be used on client's frontend
|
5
5
|
class CrudController < Apicasso::ApplicationController
|
6
|
-
before_action :set_root_resource
|
7
6
|
before_action :set_object, except: %i[index create schema]
|
8
7
|
before_action :set_nested_resource, only: %i[nested_index]
|
9
|
-
before_action :set_records, only: %i[index
|
8
|
+
before_action :set_records, only: %i[index]
|
10
9
|
include Orderable
|
11
10
|
# GET /:resource
|
12
11
|
# Returns a paginated, ordered and filtered query based response.
|
@@ -81,11 +80,6 @@ module Apicasso
|
|
81
80
|
|
82
81
|
private
|
83
82
|
|
84
|
-
# Common setup to stablish which model is the resource of this request
|
85
|
-
def set_root_resource
|
86
|
-
@root_resource = params[:resource].classify.constantize
|
87
|
-
end
|
88
|
-
|
89
83
|
# Common setup to stablish which object this request is querying
|
90
84
|
def set_object
|
91
85
|
id = params[:id]
|
@@ -93,17 +87,7 @@ module Apicasso
|
|
93
87
|
rescue NoMethodError
|
94
88
|
@object = resource.find(id)
|
95
89
|
ensure
|
96
|
-
authorize!
|
97
|
-
end
|
98
|
-
|
99
|
-
# Setup to stablish the nested model to be queried
|
100
|
-
def set_nested_resource
|
101
|
-
@nested_resource = @object.send(params[:nested].underscore.pluralize)
|
102
|
-
end
|
103
|
-
|
104
|
-
# Reutrns root_resource if nested_resource is not set scoped by permissions
|
105
|
-
def resource
|
106
|
-
(@nested_resource || @root_resource)
|
90
|
+
authorize! action_name.to_sym, @object
|
107
91
|
end
|
108
92
|
|
109
93
|
# Used to setup the resource's schema, mapping attributes and it's types
|
@@ -148,8 +132,8 @@ module Apicasso
|
|
148
132
|
@records = @records.accessible_by(current_ability).unscope(:order)
|
149
133
|
end
|
150
134
|
|
151
|
-
# The response for index action, which can be a pagination of a
|
152
|
-
# or a grouped count of attributes
|
135
|
+
# The response for index action, which can be a pagination of a
|
136
|
+
# record collection or a grouped count of attributes
|
153
137
|
def index_json
|
154
138
|
if params[:group].present?
|
155
139
|
@records.group(params[:group][:by].split(','))
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This concern is used to check SQL injection
|
4
|
+
module SqlSecurity
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
Rails.application.eager_load!
|
8
|
+
DESCENDANTS_UNDERSCORED = ActiveRecord::Base.descendants.map do |descendant|
|
9
|
+
descendant.to_s.underscore
|
10
|
+
end.freeze
|
11
|
+
|
12
|
+
GROUP_CALCULATE = %w[
|
13
|
+
average
|
14
|
+
calculate
|
15
|
+
count
|
16
|
+
ids
|
17
|
+
maximum
|
18
|
+
minimum
|
19
|
+
pluck
|
20
|
+
sum
|
21
|
+
].freeze
|
22
|
+
|
23
|
+
# Check if request is a sql injection
|
24
|
+
def sql_injection(klass)
|
25
|
+
apicasso_parameters.each do |key, value|
|
26
|
+
if key.to_sym == :group
|
27
|
+
return false unless group_sql_safe?(klass, value)
|
28
|
+
else
|
29
|
+
return false unless parameters_sql_safe?(klass, value)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Check if group params is safe for sql injection
|
37
|
+
def group_sql_safe?(klass, value)
|
38
|
+
value.each do |group_key, group_value|
|
39
|
+
if group_key.to_sym == :calculate
|
40
|
+
return false unless GROUP_CALCULATE.include?(group_value)
|
41
|
+
else
|
42
|
+
return false unless safe_for_sql?(klass, group_value)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
# Check if regular params is safe for sql injection
|
49
|
+
def parameters_sql_safe?(klass, value)
|
50
|
+
value.split(',').each do |param|
|
51
|
+
return false unless safe_for_sql?(klass, param.gsub(/\A[+-]/, ''))
|
52
|
+
end
|
53
|
+
true
|
54
|
+
end
|
55
|
+
|
56
|
+
# Check if value for current class is valid for API consumption
|
57
|
+
def safe_for_sql?(klass, value)
|
58
|
+
klass.column_names.include?(value) ||
|
59
|
+
DESCENDANTS_UNDERSCORED.include?(value) ||
|
60
|
+
klass.new.respond_to?(value) ||
|
61
|
+
klass.reflect_on_all_associations.map(&:name).include?(value)
|
62
|
+
end
|
63
|
+
|
64
|
+
def apicasso_parameters
|
65
|
+
params.to_unsafe_h.slice(:group, :resource, :nested, :sort, :include)
|
66
|
+
end
|
67
|
+
end
|
@@ -5,6 +5,9 @@ module Apicasso
|
|
5
5
|
class Ability
|
6
6
|
include CanCan::Ability
|
7
7
|
|
8
|
+
# Method that initializes CanCanCan with the scope of
|
9
|
+
# permissions based on current key from request
|
10
|
+
# @param key [Object] a key object by APIcasso to CanCanCan with ability
|
8
11
|
def initialize(key)
|
9
12
|
key ||= Apicasso::Key.new
|
10
13
|
cannot :manage, :all
|
File without changes
|
data/app/models/apicasso/key.rb
CHANGED
File without changes
|
File without changes
|
data/config/routes.rb
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
Apicasso::Engine.routes.draw do
|
2
2
|
scope module: :apicasso do
|
3
|
+
# When your application needs some kind of custom interaction that is not covered by
|
4
|
+
# APIcasso's CRUD approach, you can make your own actions using our base classes and
|
5
|
+
# objects to go straight into your logic. If you have built the APIcasso's engine into
|
6
|
+
# a route it is important that your custom action takes precedence over the gem's ones.
|
7
|
+
# Usage:
|
8
|
+
# match ' /: resource /: id / custom-action ' => ' custom # not_a_crud ' , via: :get
|
9
|
+
# mount Apicasso :: Engine , em: " / api / v1 "
|
3
10
|
resources :apidocs, only: [:index]
|
4
11
|
get '/:resource/', to: 'crud#index', via: :get
|
5
12
|
match '/:resource/', to: 'crud#create', via: :post
|
@@ -8,7 +8,11 @@ module Apicasso
|
|
8
8
|
# own application.
|
9
9
|
module ActiveRecordExtension
|
10
10
|
extend ActiveSupport::Concern
|
11
|
+
# Module with class methods of Apicasso
|
11
12
|
module ClassMethods
|
13
|
+
# Method that map validations for consumption on the Swagger JSON
|
14
|
+
# @param validation [Array] a validator to be checked
|
15
|
+
# @returns [Array] All validated attributes
|
12
16
|
def validated_attrs_for(validation)
|
13
17
|
if validation.is_a?(String) || validation.is_a?(Symbol)
|
14
18
|
klass = 'ActiveRecord::Validations::' \
|
@@ -25,6 +29,7 @@ module Apicasso
|
|
25
29
|
presence_validators.present?
|
26
30
|
end
|
27
31
|
|
32
|
+
# Method that lists all presence validators
|
28
33
|
def presence_validators
|
29
34
|
validated_attrs_for(:presence)
|
30
35
|
end
|
data/lib/apicasso/engine.rb
CHANGED
File without changes
|
data/lib/apicasso/version.rb
CHANGED
data/lib/apicasso.rb
CHANGED
File without changes
|
@@ -2,11 +2,15 @@ require 'rails/generators/migration'
|
|
2
2
|
|
3
3
|
module Apicasso
|
4
4
|
module Generators
|
5
|
+
# Class used to install Apicasso engine into a project
|
5
6
|
class InstallGenerator < ::Rails::Generators::Base
|
6
7
|
include Rails::Generators::Migration
|
7
8
|
source_root File.expand_path('../templates', __FILE__)
|
8
9
|
desc 'Add the required migrations to run APIcasso'
|
9
10
|
|
11
|
+
# Method generates the next migration number
|
12
|
+
# @param path [String] the path to migration directory
|
13
|
+
# @returns [String] the next migration number
|
10
14
|
def self.next_migration_number(path)
|
11
15
|
if @prev_migration_nr
|
12
16
|
@prev_migration_nr += 1
|
@@ -16,6 +20,8 @@ module Apicasso
|
|
16
20
|
@prev_migration_nr.to_s
|
17
21
|
end
|
18
22
|
|
23
|
+
# Create a migration to setup database tables used by the
|
24
|
+
# engine to implement authentication, authorization and auditability
|
19
25
|
def copy_migrations
|
20
26
|
migration_template 'create_apicasso_tables.rb',
|
21
27
|
'db/migrate/create_apicasso_tables.rb'
|
@@ -1,8 +1,13 @@
|
|
1
|
+
# Migration from APIcasso tables
|
1
2
|
class CreateApicassoTables < ActiveRecord::Migration[5.0]
|
3
|
+
# Method that generates migration apicasso_keys and apicasso_keys tables
|
2
4
|
def change
|
3
5
|
execute <<-SQL
|
4
6
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
5
7
|
SQL
|
8
|
+
# The apicasso_keys schema to creates the table
|
9
|
+
# Models will are exposed based on definitions setted in :scope
|
10
|
+
# The objects will are manageable through :token
|
6
11
|
create_table :apicasso_keys, id: :uuid do |t|
|
7
12
|
t.json :scope
|
8
13
|
t.integer :scope_type
|
@@ -11,6 +16,9 @@ class CreateApicassoTables < ActiveRecord::Migration[5.0]
|
|
11
16
|
t.datetime :deleted_at
|
12
17
|
t.timestamps null: false
|
13
18
|
end
|
19
|
+
# The apicasso_requests schema to creates the table
|
20
|
+
# All requests will be saved into this table
|
21
|
+
# Thus, available for use in an audit
|
14
22
|
create_table :apicasso_requests, id: :uuid do |t|
|
15
23
|
t.text :api_key_id
|
16
24
|
t.json :object
|
data/spec/apicasso_spec.rb
CHANGED
File without changes
|
data/spec/dummy/Gemfile
CHANGED
File without changes
|
data/spec/dummy/Gemfile.lock
CHANGED
File without changes
|
data/spec/dummy/Rakefile
CHANGED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
data/spec/dummy/bin/bundle
CHANGED
File without changes
|
data/spec/dummy/bin/rails
CHANGED
File without changes
|
data/spec/dummy/bin/rake
CHANGED
File without changes
|
data/spec/dummy/bin/setup
CHANGED
File without changes
|
data/spec/dummy/bin/spring
CHANGED
File without changes
|
data/spec/dummy/bin/update
CHANGED
File without changes
|
File without changes
|
data/spec/dummy/config/boot.rb
CHANGED
File without changes
|
data/spec/dummy/config/cable.yml
CHANGED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
data/spec/dummy/config/puma.rb
CHANGED
File without changes
|
data/spec/dummy/config/routes.rb
CHANGED
File without changes
|
data/spec/dummy/config/spring.rb
CHANGED
File without changes
|
File without changes
|
data/spec/dummy/config.ru
CHANGED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
data/spec/dummy/db/schema.rb
CHANGED
File without changes
|
data/spec/dummy/db/seeds.rb
CHANGED
File without changes
|
data/spec/dummy/package.json
CHANGED
File without changes
|
File without changes
|
File without changes
|
data/spec/rails_helper.rb
CHANGED
File without changes
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
4
|
+
RSpec.describe 'Used Model bad requests', type: :request do
|
5
|
+
token = Apicasso::Key.create(scope: { manage: { used_model: true } }).token
|
6
|
+
access_token = { 'AUTHORIZATION' => "Token token=#{token}" }
|
7
|
+
|
8
|
+
context 'raise a bad request when using SQL injection' do
|
9
|
+
it 'for grouping in fields' do
|
10
|
+
expect {
|
11
|
+
get '/api/v1/used_model', params: {
|
12
|
+
'group[by]': 'brand',
|
13
|
+
'group[calculate]': 'count',
|
14
|
+
'group[fields]': "'OR 1=1;"
|
15
|
+
}, headers: access_token
|
16
|
+
}.to raise_exception(ActionController::BadRequest)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'for sorting' do
|
20
|
+
expect {
|
21
|
+
get '/api/v1/used_model', params: { 'per_page': -1, 'sort': "'OR 1=1;" }, headers: access_token
|
22
|
+
}.to raise_exception(ActionController::BadRequest)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'for include' do
|
26
|
+
expect {
|
27
|
+
get '/api/v1/used_model', params: { 'include': "'OR 1=1;" }, headers: access_token
|
28
|
+
}.to raise_exception(ActionController::BadRequest)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'raise a bad request when using invalid resources' do
|
33
|
+
it 'for root resource' do
|
34
|
+
expect {
|
35
|
+
get '/api/v1/admins', headers: access_token
|
36
|
+
}.to raise_exception(ActionController::BadRequest)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'for nested resource' do
|
40
|
+
expect {
|
41
|
+
get '/api/v1/used_model/1/admins', headers: access_token
|
42
|
+
}.to raise_exception(ActionController::BadRequest)
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'for include' do
|
46
|
+
expect {
|
47
|
+
get '/api/v1/used_model', params: { 'include': 'admins' }, headers: access_token
|
48
|
+
}.to raise_exception(ActionController::BadRequest)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|