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