arsi 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 96250290ae58d2100a2c289b457c91606ff9838e
4
+ data.tar.gz: ae74117e3ce3ae1dd1aa1a3b01a940969bf90e4e
5
+ SHA512:
6
+ metadata.gz: 9823fe65e3ccdda88e3eeaad33e2abe8ac2990af40e88339e15430b75ab0f2784c651e5e36b1c892dd323982b833ff3970e07f44187a7c4b43692d8ba6aebb5c
7
+ data.tar.gz: cc2326555d8bac5e1af14d03ab8052acd30b41a9532c4d73d400305e04dda7280147e5860a94405f320a4e4750cb9e355aadf26758b54daa0f300cb3c6fcefa6
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # ARSI - ActiveRecord SQL Inspector
2
+
3
+ [![Build Status](https://magnum.travis-ci.com/zendesk/arsi.svg?token=MsU5XFxeU3atFLQoVGDv&branch=master)](https://magnum.travis-ci.com/zendesk/arsi)
4
+
5
+ Block sql statements that are not scoped by id in `.update_all` and `.delete_all`.
6
+
7
+ ID Columns:
8
+
9
+ - *_id
10
+ - id
11
+ - guid
12
+ - uuid
13
+ - uid
14
+
15
+ Operators:
16
+
17
+ - =
18
+ - <>
19
+ - IN
20
+ - IS
21
+
22
+ Triggers the `Arsi.violation_callback` with SQL and relation object.By default raise `Arsi::UnscopedSQL`.
23
+
24
+ ## Disabling
25
+
26
+ via `.without_arsi`
27
+
28
+ ```ruby
29
+ User.where(active: false).without_arsi.delete_all # I know what I'm doing...
30
+
31
+ ```
32
+
33
+ via `ARSI.disable`
34
+
35
+ ```ruby
36
+ class ApplicationController < ActionController::Base
37
+ around_filter :without_arsi
38
+ def without_arsi(&block)
39
+ Arsi.disable(&block)
40
+ end
41
+ end
42
+
43
+ Arsi.disable do
44
+ User.update_all name: "Pete" # will be ignored
45
+ end
46
+ ```
47
+
48
+
49
+ ## Limitations
50
+
51
+ - MySQL
52
+ - uses regexs on SQL, false negatives with specially crafted SQL statements can occur
data/lib/arsi.rb ADDED
@@ -0,0 +1,68 @@
1
+ require 'arsi/arel_tree_manager'
2
+ require 'arsi/mysql2_adapter'
3
+ require 'arsi/relation'
4
+ require 'arsi/table'
5
+ require 'active_record'
6
+ require 'active_record/connection_adapters/mysql2_adapter'
7
+
8
+ module Arsi
9
+ class UnscopedSQL < StandardError; end
10
+ Arel::TreeManager.send(:include, ArelTreeManager)
11
+ ActiveRecord::ConnectionAdapters::Mysql2Adapter.send(:prepend, Mysql2Adapter)
12
+ ActiveRecord::Relation.send(:prepend, Relation)
13
+ ActiveRecord::Querying.delegate(:without_arsi, :to => :relation)
14
+ Arel::Table.send(:prepend, Table) if ActiveRecord::VERSION::MAJOR >= 4
15
+
16
+ @enabled = true
17
+
18
+ ID_MATCH = "(gu|uu|u)?id"
19
+ SCOPEABLE_REGEX = /(^|_)#{ID_MATCH}$/i # http://rubular.com/r/hPVpG9jyoC
20
+ SQL_MATCHER = /[\s_`(]#{ID_MATCH}`?\s+(=|<>|IN|IS)/i # http://rubular.com/r/7xuhnBiOgs
21
+ DEFAULT_CALLBACK = lambda do |sql, relation|
22
+ raise UnscopedSQL, "Missing ID in the where sql:\n#{sql}\nAdd id or use without_arsi"
23
+ end
24
+
25
+ class << self
26
+ attr_accessor :violation_callback
27
+
28
+ def sql_check!(sql, relation)
29
+ return if !@enabled || relation.try(:without_arsi?)
30
+ return if sql =~ SQL_MATCHER
31
+ report_violation(sql, relation)
32
+ end
33
+
34
+ def arel_check!(arel, relation)
35
+ sql = arel.respond_to?(:ast) ? arel.where_sql : arel.to_s
36
+ sql_check!(sql, relation)
37
+ end
38
+
39
+ def disable!
40
+ @enabled = false
41
+ end
42
+
43
+ def enable!
44
+ @enabled = true
45
+ end
46
+
47
+ def disable(&block)
48
+ run_with_arsi(false, &block)
49
+ end
50
+
51
+ def enable(&block)
52
+ run_with_arsi(true, &block)
53
+ end
54
+
55
+ private
56
+
57
+ def run_with_arsi(with_arsi)
58
+ previous, @enabled = @enabled, with_arsi
59
+ yield
60
+ ensure
61
+ @enabled = previous
62
+ end
63
+
64
+ def report_violation(sql, relation)
65
+ (violation_callback || DEFAULT_CALLBACK).call(sql, relation)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,21 @@
1
+ require 'arel'
2
+
3
+ module Arsi
4
+ module ArelTreeManager
5
+ # This is from Arel::SelectManager which inherits from Arel::TreeManager.
6
+ # We need where_sql on both Arel::UpdateManager and Arel::DeleteManager so we add it to the parent class.
7
+ if ::Arel::VERSION.start_with?('6')
8
+ def where_sql
9
+ return if @ctx.wheres.empty?
10
+ viz = ::Arel::Visitors::WhereSql.new @engine.connection
11
+ ::Arel::Nodes::SqlLiteral.new viz.accept(@ctx, ::Arel::Collectors::SQLString.new).value
12
+ end
13
+ else
14
+ def where_sql
15
+ return if @ctx.wheres.empty?
16
+ viz = ::Arel::Visitors::WhereSql.new @engine.connection
17
+ ::Arel::Nodes::SqlLiteral.new viz.accept @ctx
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ module Arsi
2
+ module Mysql2Adapter
3
+ attr_accessor :arsi_relation
4
+
5
+ def delete(arel, *)
6
+ Arsi.arel_check!(arel, arsi_relation)
7
+ super
8
+ end
9
+
10
+ def update(arel, *)
11
+ Arsi.arel_check!(arel, arsi_relation)
12
+ super
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,46 @@
1
+ module Arsi
2
+ module Relation
3
+ attr_accessor :without_arsi
4
+
5
+ def without_arsi
6
+ if block_given?
7
+ raise "Use without_arsi in a chain. Don't pass it a block"
8
+ end
9
+ dup.tap(&:without_arsi!)
10
+ end
11
+
12
+ def without_arsi!
13
+ @without_arsi = true
14
+ end
15
+
16
+ def without_arsi?
17
+ @without_arsi || !arsi_scopeable?
18
+ end
19
+
20
+ def delete_all(*)
21
+ with_relation_in_connection { super }
22
+ end
23
+
24
+ def self.prepended(base)
25
+ base.class_eval do
26
+ alias_method :update_all_without_arsi, :update_all
27
+ def update_all(*args)
28
+ with_relation_in_connection { update_all_without_arsi(*args) }
29
+ end
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def arsi_scopeable?
36
+ table.columns.map(&:name).any? { |c| c =~ Arsi::SCOPEABLE_REGEX }
37
+ end
38
+
39
+ def with_relation_in_connection
40
+ @klass.connection.arsi_relation = self
41
+ yield
42
+ ensure
43
+ @klass.connection.arsi_relation = nil
44
+ end
45
+ end
46
+ end
data/lib/arsi/table.rb ADDED
@@ -0,0 +1,5 @@
1
+ module Arsi::Table
2
+ def columns
3
+ @columns ||= attributes_for @engine.connection.columns(@name)
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module Arsi
2
+ VERSION = '0.2.0'
3
+ end
metadata ADDED
@@ -0,0 +1,211 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: arsi
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Christopher Kintner
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-08-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: arel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: mysql2
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activerecord
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">"
46
+ - !ruby/object:Gem::Version
47
+ version: 3.2.15
48
+ - - "<"
49
+ - !ruby/object:Gem::Version
50
+ version: 4.3.0
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">"
56
+ - !ruby/object:Gem::Version
57
+ version: 3.2.15
58
+ - - "<"
59
+ - !ruby/object:Gem::Version
60
+ version: 4.3.0
61
+ - !ruby/object:Gem::Dependency
62
+ name: bump
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: bundler
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: rake
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: minitest
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ - !ruby/object:Gem::Dependency
118
+ name: minitest-rg
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ - !ruby/object:Gem::Dependency
132
+ name: mocha
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ - !ruby/object:Gem::Dependency
146
+ name: wwtd
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ type: :development
153
+ prerelease: false
154
+ version_requirements: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ - !ruby/object:Gem::Dependency
160
+ name: byebug
161
+ requirement: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ type: :development
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ description: Puts your SQL under a microscope
174
+ email:
175
+ - ckintner@zendesk.com
176
+ executables: []
177
+ extensions: []
178
+ extra_rdoc_files: []
179
+ files:
180
+ - README.md
181
+ - lib/arsi.rb
182
+ - lib/arsi/arel_tree_manager.rb
183
+ - lib/arsi/mysql2_adapter.rb
184
+ - lib/arsi/relation.rb
185
+ - lib/arsi/table.rb
186
+ - lib/arsi/version.rb
187
+ homepage: https://github.com/zendesk/arsi
188
+ licenses:
189
+ - Apache License Version 2.0
190
+ metadata: {}
191
+ post_install_message:
192
+ rdoc_options: []
193
+ require_paths:
194
+ - lib
195
+ required_ruby_version: !ruby/object:Gem::Requirement
196
+ requirements:
197
+ - - ">="
198
+ - !ruby/object:Gem::Version
199
+ version: '0'
200
+ required_rubygems_version: !ruby/object:Gem::Requirement
201
+ requirements:
202
+ - - ">="
203
+ - !ruby/object:Gem::Version
204
+ version: '0'
205
+ requirements: []
206
+ rubyforge_project:
207
+ rubygems_version: 2.2.2
208
+ signing_key:
209
+ specification_version: 4
210
+ summary: ActiveRecord SQL Inspector
211
+ test_files: []