apicasso 0.4.11 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -3
  3. data/Rakefile +0 -0
  4. data/app/controllers/apicasso/apidocs_controller.rb +332 -326
  5. data/app/controllers/apicasso/application_controller.rb +46 -1
  6. data/app/controllers/apicasso/crud_controller.rb +4 -20
  7. data/app/controllers/concerns/orderable.rb +1 -1
  8. data/app/controllers/concerns/sql_security.rb +67 -0
  9. data/app/models/apicasso/ability.rb +3 -0
  10. data/app/models/apicasso/application_record.rb +0 -0
  11. data/app/models/apicasso/key.rb +0 -0
  12. data/app/models/apicasso/request.rb +0 -0
  13. data/config/routes.rb +7 -0
  14. data/lib/apicasso/active_record_extension.rb +5 -0
  15. data/lib/apicasso/engine.rb +0 -0
  16. data/lib/apicasso/version.rb +1 -1
  17. data/lib/apicasso.rb +0 -0
  18. data/lib/generators/apicasso/install/install_generator.rb +6 -0
  19. data/lib/generators/apicasso/install/templates/create_apicasso_tables.rb +8 -0
  20. data/spec/apicasso_spec.rb +0 -0
  21. data/spec/dummy/Gemfile +0 -0
  22. data/spec/dummy/Gemfile.lock +0 -0
  23. data/spec/dummy/Rakefile +0 -0
  24. data/spec/dummy/app/controllers/application_controller.rb +0 -0
  25. data/spec/dummy/app/models/application_record.rb +0 -0
  26. data/spec/dummy/app/models/used_model.rb +0 -0
  27. data/spec/dummy/bin/bundle +0 -0
  28. data/spec/dummy/bin/rails +0 -0
  29. data/spec/dummy/bin/rake +0 -0
  30. data/spec/dummy/bin/setup +0 -0
  31. data/spec/dummy/bin/spring +0 -0
  32. data/spec/dummy/bin/update +0 -0
  33. data/spec/dummy/config/application.rb +0 -0
  34. data/spec/dummy/config/boot.rb +0 -0
  35. data/spec/dummy/config/cable.yml +0 -0
  36. data/spec/dummy/config/credentials.yml.enc +0 -0
  37. data/spec/dummy/config/database.yml +0 -0
  38. data/spec/dummy/config/environment.rb +0 -0
  39. data/spec/dummy/config/environments/development.rb +0 -0
  40. data/spec/dummy/config/environments/production.rb +0 -0
  41. data/spec/dummy/config/environments/test.rb +0 -0
  42. data/spec/dummy/config/initializers/application_controller_renderer.rb +0 -0
  43. data/spec/dummy/config/initializers/backtrace_silencers.rb +0 -0
  44. data/spec/dummy/config/initializers/cors.rb +0 -0
  45. data/spec/dummy/config/initializers/filter_parameter_logging.rb +0 -0
  46. data/spec/dummy/config/initializers/inflections.rb +0 -0
  47. data/spec/dummy/config/initializers/mime_types.rb +0 -0
  48. data/spec/dummy/config/initializers/wrap_parameters.rb +0 -0
  49. data/spec/dummy/config/locales/en.yml +0 -0
  50. data/spec/dummy/config/puma.rb +0 -0
  51. data/spec/dummy/config/routes.rb +0 -0
  52. data/spec/dummy/config/spring.rb +0 -0
  53. data/spec/dummy/config/storage.yml +0 -0
  54. data/spec/dummy/config.ru +0 -0
  55. data/spec/dummy/db/migrate/20180918134607_create_apicasso_tables.rb +0 -0
  56. data/spec/dummy/db/migrate/20180918141254_create_used_models.rb +0 -0
  57. data/spec/dummy/db/migrate/20180919130152_create_active_storage_tables.active_storage.rb +0 -0
  58. data/spec/dummy/db/migrate/20180920133933_change_used_model_to_validates.rb +0 -0
  59. data/spec/dummy/db/schema.rb +0 -0
  60. data/spec/dummy/db/seeds.rb +0 -0
  61. data/spec/dummy/package.json +0 -0
  62. data/spec/factories/used_model.rb +0 -0
  63. data/spec/models/used_model_spec.rb +0 -0
  64. data/spec/rails_helper.rb +0 -0
  65. data/spec/requests/bad_requests_spec.rb +51 -0
  66. data/spec/requests/requests_spec.rb +98 -23
  67. data/spec/spec_helper.rb +1 -1
  68. data/spec/support/database_cleaner.rb +8 -0
  69. data/spec/support/factory_bot.rb +0 -0
  70. data/spec/token/token_spec.rb +322 -0
  71. metadata +32 -27
  72. 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 @records.column_names.include?(field)
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 nested_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! :read, @object
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 record collection
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(','))
@@ -40,6 +40,6 @@ module Orderable
40
40
  end
41
41
 
42
42
  def model
43
- (params[:nested] || params[:resource] || controller_name).classify.constantize
43
+ representative_resource.classify.constantize
44
44
  end
45
45
  end
@@ -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
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
File without changes
@@ -1,3 +1,3 @@
1
1
  module Apicasso
2
- VERSION = '0.4.11'.freeze
2
+ VERSION = '0.5.0'.freeze
3
3
  end
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
File without changes
data/spec/dummy/Gemfile CHANGED
File without changes
File without changes
data/spec/dummy/Rakefile CHANGED
File without changes
File without changes
File without changes
File without changes
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
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
File without changes
File without changes
File without changes
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
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