arsi 0.2.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 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: []