guardrail 2.0.0 → 3.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 737326a16630468262a493961348ecc21dd8b4f8d82c6526bfafe1c0b66c294c
4
- data.tar.gz: c90574a6e50c6556e678b08acc766ef74c36be693e6d012989bb6fcb71e93c77
3
+ metadata.gz: 356cff039858ba07cbc234ab54cf80c96c8211a2dea702c020a48fd108dc87ae
4
+ data.tar.gz: 768e3d4069f07db8e7c0d67eb1c858a826977c0e8261604217fbd87580afb33b
5
5
  SHA512:
6
- metadata.gz: d9761c6d3817b19a980827c71fceca3051cc8cbaf2b86bd959bf110f954a18c271e433bd433d92352d455e6af38234a1945369bbac118b142e1bcdfe45bbd78d
7
- data.tar.gz: 20aab4abcd5ea1743df249d58d7772ac186277d6e8eb25e9772dd7aed46e41fabc1ac1870a4e802c048428f27f2bb7eb38bbbeffc1c951dd22e177c5f92d54ab
6
+ metadata.gz: 9aab9b92d9de1ecd7867085b2938686e02e4ef3caa37a04b9e2d8ddb0ebcb3395cfca0e95e14559a5694ef3e0b5de83d61478eaccac1c9169240e0c521fb72c8
7
+ data.tar.gz: '02464494bbb4c25425154b4c9bb1958938becb94da6678bfe6af2baf42b557cb4be62b75bf79aae280eee685629c8fb5799298f3681aeceb72a9b321aac03de7'
data/README.md CHANGED
@@ -3,55 +3,27 @@ GuardRail
3
3
 
4
4
  ## About
5
5
 
6
- GuardRail allows multiple database environments and environment overrides to
7
- ActiveRecord, allowing least-privilege best practices.
6
+ GuardRail is a thin wrapper around Rail 6.1's native role switching.
8
7
 
9
8
  ## Installation
10
9
 
11
- Add `gem 'guardrail'` to your Gemfile (tested with Rails 4.x and 5.x, and also
12
- with the release candidate for Rails 6.0)
10
+ Add `gem 'guardrail'` to your Gemfile.
13
11
 
14
12
  ## Usage
15
13
 
16
- There are two major use cases for guardrail. The first is for primary/replica(/deploy) environments.
17
- Using a replica is as simple as adding a replica block (underneath your main environment block) in
18
- database.yml, then wrapping stuff you want to query the replica in GuardRail.activate(:replica) blocks.
19
- You can extend this to a deploy environment so that migrations will run as a deploy user that
20
- permission to modify schema, while your normal app runs with lower privileges that cannot modify
21
- schema. This is defense-in-depth in practice, so that *if* you happen to have a SQL injection
22
- bug, it would be impossible to do something like dropping tables.
14
+ See https://guides.rubyonrails.org/active_record_multiple_databases.html. GuardRail simply adds
15
+ syntactic sugar to easily switch to different roles:
23
16
 
24
- The other major use case is more defense-in-depth. By carefully setting up your environment, you
25
- can default to script/console sessions for regular users to use their own database user, and the
26
- replica.
27
-
28
- Example database.yml file:
29
-
30
- ```yaml
31
- production:
32
- adapter: postgresql
33
- username: myapp
34
- database: myapp
35
- host: db-primary
36
- replica:
37
- host: db-replica
38
- deploy:
39
- username: deploy
40
- ```
41
-
42
- Using an initializer, you can achieve the default environment settings (in tandem with profile
43
- changes):
44
17
 
45
18
  ```ruby
46
- if ENV['RAILS_DATABASE_ENVIRONMENT']
47
- GuardRail.activate!(ENV['RAILS_DATABASE_ENVIRONMENT'].to_sym)
48
- end
49
- if ENV['RAILS_DATABASE_USER']
50
- GuardRail.apply_config!(:username => ENV['RAILS_DATABASE_USER'])
19
+ def some_method
20
+ GuardRails.activate(:secondary) do
21
+ MyModel.some_really_long_query
22
+ end
51
23
  end
52
24
  ```
53
25
 
54
- Additionally **in Ruby 2.0+** you can include GuardRail::HelperMethods and use several helpers
26
+ Additionally you can include GuardRail::HelperMethods and use several helpers
55
27
  to execute methods on specific environments:
56
28
 
57
29
  ```ruby
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module GuardRail
2
4
  module HelperMethods
3
5
  def self.included(base)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module GuardRail
2
- VERSION = "2.0.0"
4
+ VERSION = "3.0.1"
3
5
  end
data/lib/guard_rail.rb CHANGED
@@ -1,114 +1,19 @@
1
- require 'set'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module GuardRail
4
4
  class << self
5
- attr_accessor :primary_environment_name
6
- GuardRail.primary_environment_name = :primary
7
-
8
5
  def environment
9
- Thread.current[:guard_rail_environment] ||= primary_environment_name
10
- end
11
-
12
- def global_config
13
- @global_config ||= {}
14
- end
15
-
16
- def activated_environments
17
- @activated_environments ||= Set.new()
18
- end
19
-
20
- # semi-private
21
- def initialize!
22
- require 'guard_rail/connection_handler'
23
- require 'guard_rail/connection_specification'
24
- require 'guard_rail/helper_methods'
25
-
26
- activated_environments << GuardRail.environment
27
-
28
- ActiveRecord::ConnectionAdapters::ConnectionHandler.prepend(ConnectionHandler)
29
- ActiveRecord::ConnectionAdapters::ConnectionSpecification.prepend(ConnectionSpecification)
30
- end
31
-
32
- def global_config_sequence
33
- @global_config_sequence ||= 1
34
- end
35
-
36
- def bump_sequence
37
- @global_config_sequence ||= 1
38
- @global_config_sequence += 1
39
- ActiveRecord::Base::connection_handler.clear_all_connections!
40
- end
41
-
42
- # for altering other pieces of config (i.e. username)
43
- # will force a disconnect
44
- def apply_config!(hash)
45
- global_config.merge!(hash)
46
- bump_sequence
47
- end
48
-
49
- def remove_config!(key)
50
- global_config.delete(key)
51
- bump_sequence
52
- end
53
-
54
- def connection_handlers
55
- save_handler
56
- @connection_handlers
57
- end
58
-
59
- # switch environment for the duration of the block
60
- # will keep the old connections around
61
- def activate(environment)
62
- environment ||= primary_environment_name
63
- return yield if environment == self.environment
64
- begin
65
- old_environment = activate!(environment)
66
- activated_environments << environment
67
- yield
68
- ensure
69
- Thread.current[:guard_rail_environment] = old_environment
70
- ActiveRecord::Base.connection_handler = ensure_handler unless test?
71
- end
6
+ ActiveRecord::Base.current_role
72
7
  end
73
8
 
74
- # for use from script/console ONLY
75
- def activate!(environment)
76
- environment ||= primary_environment_name
77
- save_handler
78
- old_environment = self.environment
79
- Thread.current[:guard_rail_environment] = environment
80
- ActiveRecord::Base.connection_handler = ensure_handler unless test?
81
- old_environment
9
+ def activate(role)
10
+ return yield if environment == role
11
+ ActiveRecord::Base.connected_to(role: role) { yield }
82
12
  end
83
13
 
84
- private
85
-
86
- def test?
87
- Rails.env.test?
88
- end
89
-
90
- def save_handler
91
- @connection_handlers ||= {}
92
- @connection_handlers[environment] ||= ActiveRecord::Base.connection_handler
93
- end
94
-
95
- def ensure_handler
96
- new_handler = @connection_handlers[environment]
97
- if !new_handler
98
- new_handler = @connection_handlers[environment] = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
99
- pools = ActiveRecord::Base.connection_handler.send(:owner_to_pool)
100
- pools.each_pair do |model, pool|
101
- new_handler.establish_connection(pool.spec.config)
102
- end
103
- end
104
- new_handler
14
+ def activate!(role)
15
+ return if environment == role
16
+ ActiveRecord::Base.connecting_to(role: role)
105
17
  end
106
18
  end
107
19
  end
108
-
109
- if defined?(Rails::Railtie)
110
- require "guard_rail/railtie"
111
- else
112
- # just load everything immediately for Rails 2
113
- GuardRail.initialize!
114
- end
data/lib/guardrail.rb CHANGED
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'guard_rail'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: guardrail
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 3.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-05 00:00:00.000000000 Z
11
+ date: 2022-03-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,40 +16,40 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '5.1'
19
+ version: '6.1'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '6.1'
22
+ version: '7.1'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: '5.1'
29
+ version: '6.1'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '6.1'
32
+ version: '7.1'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: railties
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: '5.1'
39
+ version: '6.1'
40
40
  - - "<"
41
41
  - !ruby/object:Gem::Version
42
- version: '6.1'
42
+ version: '7.1'
43
43
  type: :runtime
44
44
  prerelease: false
45
45
  version_requirements: !ruby/object:Gem::Requirement
46
46
  requirements:
47
47
  - - ">="
48
48
  - !ruby/object:Gem::Version
49
- version: '5.1'
49
+ version: '6.1'
50
50
  - - "<"
51
51
  - !ruby/object:Gem::Version
52
- version: '6.1'
52
+ version: '7.1'
53
53
  - !ruby/object:Gem::Dependency
54
54
  name: appraisal
55
55
  requirement: !ruby/object:Gem::Requirement
@@ -78,48 +78,6 @@ dependencies:
78
78
  - - ">="
79
79
  - !ruby/object:Gem::Version
80
80
  version: '0'
81
- - !ruby/object:Gem::Dependency
82
- name: mocha
83
- requirement: !ruby/object:Gem::Requirement
84
- requirements:
85
- - - ">="
86
- - !ruby/object:Gem::Version
87
- version: '0'
88
- type: :development
89
- prerelease: false
90
- version_requirements: !ruby/object:Gem::Requirement
91
- requirements:
92
- - - ">="
93
- - !ruby/object:Gem::Version
94
- version: '0'
95
- - !ruby/object:Gem::Dependency
96
- name: rspec
97
- requirement: !ruby/object:Gem::Requirement
98
- requirements:
99
- - - "~>"
100
- - !ruby/object:Gem::Version
101
- version: '3.0'
102
- type: :development
103
- prerelease: false
104
- version_requirements: !ruby/object:Gem::Requirement
105
- requirements:
106
- - - "~>"
107
- - !ruby/object:Gem::Version
108
- version: '3.0'
109
- - !ruby/object:Gem::Dependency
110
- name: sqlite3
111
- requirement: !ruby/object:Gem::Requirement
112
- requirements:
113
- - - ">="
114
- - !ruby/object:Gem::Version
115
- version: '0'
116
- type: :development
117
- prerelease: false
118
- version_requirements: !ruby/object:Gem::Requirement
119
- requirements:
120
- - - ">="
121
- - !ruby/object:Gem::Version
122
- version: '0'
123
81
  description: Allows multiple environments in database.yml, and dynamically switching
124
82
  them.
125
83
  email: cody@instructure.com
@@ -130,13 +88,9 @@ files:
130
88
  - LICENSE
131
89
  - README.md
132
90
  - lib/guard_rail.rb
133
- - lib/guard_rail/connection_handler.rb
134
- - lib/guard_rail/connection_specification.rb
135
91
  - lib/guard_rail/helper_methods.rb
136
- - lib/guard_rail/railtie.rb
137
92
  - lib/guard_rail/version.rb
138
93
  - lib/guardrail.rb
139
- - spec/guard_rail_spec.rb
140
94
  homepage: http://github.com/instructure/guardrail
141
95
  licenses:
142
96
  - MIT
@@ -156,9 +110,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
156
110
  - !ruby/object:Gem::Version
157
111
  version: '0'
158
112
  requirements: []
159
- rubygems_version: 3.1.2
113
+ rubygems_version: 3.1.4
160
114
  signing_key:
161
115
  specification_version: 4
162
116
  summary: ActiveRecord database environment switching for secondaries and least-privilege
163
- test_files:
164
- - spec/guard_rail_spec.rb
117
+ test_files: []
@@ -1,13 +0,0 @@
1
- module GuardRail
2
- module ConnectionHandler
3
- %w{clear_active_connections clear_reloadable_connections
4
- clear_all_connections verify_active_connections }.each do |method|
5
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
6
- def #{method}!(super_method: false)
7
- return super() if super_method
8
- ::GuardRail.connection_handlers.values.each { |handler| handler.#{method}!(super_method: true) }
9
- end
10
- RUBY
11
- end
12
- end
13
- end
@@ -1,77 +0,0 @@
1
- require 'i18n/core_ext/hash' unless Hash.method_defined?(:deep_symbolize_keys)
2
-
3
- module GuardRail
4
- module ConnectionSpecification
5
- class CacheCoherentHash < Hash
6
- def initialize(spec)
7
- @spec = spec
8
- super
9
- end
10
-
11
- def []=(key, value)
12
- super
13
- @spec.instance_variable_set(:@current_config, nil)
14
- @spec.instance_variable_get(:@config)[key] = value
15
- end
16
-
17
- def delete(key)
18
- super
19
- @spec.instance_variable_set(:@current_config, nil)
20
- @spec.instance_variable_get(:@config).delete(key)
21
- end
22
-
23
- def dup
24
- Hash[self]
25
- end
26
-
27
- # in rails 4.2, active support tries to create a copy of the original object's class
28
- # instead of making a new Hash object, so it fails since initialize expects an argument
29
- def transform_keys(&block)
30
- dup.transform_keys(&block)
31
- end
32
- end
33
-
34
- def initialize(name, config, adapter_method)
35
- super(name, config.deep_symbolize_keys, adapter_method)
36
- end
37
-
38
- def initialize_dup(original)
39
- @current_config = nil
40
- super
41
- end
42
-
43
- def config
44
- @current_config = nil if GuardRail.environment != @current_config_environment || GuardRail.global_config_sequence != @current_config_sequence
45
- return @current_config if @current_config
46
-
47
- @current_config_environment = GuardRail.environment
48
- @current_config_sequence = GuardRail.global_config_sequence
49
- config = @config.dup
50
- if @config.has_key?(GuardRail.environment)
51
- env_config = @config[GuardRail.environment]
52
- # an array of databases for this environment; for now, just choose the first non-nil element
53
- if env_config.is_a?(Array)
54
- env_config = env_config.detect { |individual_config| !individual_config.nil? }
55
- end
56
- config.merge!(env_config.symbolize_keys)
57
- end
58
-
59
- config.keys.each do |key|
60
- next unless config[key].is_a?(String)
61
- config[key] = config[key] % config
62
- end
63
-
64
- config.merge!(GuardRail.global_config)
65
-
66
- @current_config = CacheCoherentHash.new(self)
67
- @current_config.replace(config)
68
-
69
- @current_config
70
- end
71
-
72
- def config=(value)
73
- @config = value
74
- @current_config = nil
75
- end
76
- end
77
- end
@@ -1,9 +0,0 @@
1
- module GuardRail
2
- class Railtie < Rails::Railtie
3
- initializer "guard_rail.extend_ar", :before => "active_record.initialize_database" do
4
- ActiveSupport.on_load(:active_record) do
5
- GuardRail.initialize!
6
- end
7
- end
8
- end
9
- end
@@ -1,167 +0,0 @@
1
- require 'active_record'
2
- require 'byebug'
3
- require 'rails'
4
- require 'guard_rail'
5
-
6
- # we're not actually bringing up ActiveRecord, so we need to initialize our stuff
7
- GuardRail.initialize!
8
-
9
- RSpec.configure do |config|
10
- config.mock_framework = :mocha
11
- end
12
-
13
- describe GuardRail do
14
- ConnectionSpecification = ActiveRecord::ConnectionAdapters::ConnectionSpecification
15
-
16
- def spec_args(conf, adapter)
17
- ['dummy', conf, adapter]
18
- end
19
-
20
- it "should allow changing environments" do
21
- conf = {
22
- :adapter => 'postgresql',
23
- :database => 'primary',
24
- :username => 'canvas',
25
- :deploy => {
26
- :username => 'deploy'
27
- },
28
- :replica => {
29
- :database => 'replica'
30
- }
31
- }
32
- spec = ConnectionSpecification.new(*spec_args(conf, 'adapter'))
33
- expect(spec.config[:username]).to eq('canvas')
34
- expect(spec.config[:database]).to eq('primary')
35
- GuardRail.activate(:deploy) do
36
- expect(spec.config[:username]).to eq('deploy')
37
- expect(spec.config[:database]).to eq('primary')
38
- end
39
- expect(spec.config[:username]).to eq('canvas')
40
- expect(spec.config[:database]).to eq('primary')
41
- GuardRail.activate(:replica) do
42
- expect(spec.config[:username]).to eq('canvas')
43
- expect(spec.config[:database]).to eq('replica')
44
- end
45
- expect(spec.config[:username]).to eq('canvas')
46
- expect(spec.config[:database]).to eq('primary')
47
- end
48
-
49
- it "should allow using hash insertions" do
50
- conf = {
51
- :adapter => 'postgresql',
52
- :database => 'primary',
53
- :username => '%{schema_search_path}',
54
- :schema_search_path => 'canvas',
55
- :deploy => {
56
- :username => 'deploy'
57
- }
58
- }
59
- spec = ConnectionSpecification.new(*spec_args(conf, 'adapter'))
60
- expect(spec.config[:username]).to eq('canvas')
61
- GuardRail.activate(:deploy) do
62
- expect(spec.config[:username]).to eq('deploy')
63
- end
64
- expect(spec.config[:username]).to eq('canvas')
65
- end
66
-
67
- it "should be cache coherent with modifying the config" do
68
- conf = {
69
- :adapter => 'postgresql',
70
- :database => 'primary',
71
- :username => '%{schema_search_path}',
72
- :schema_search_path => 'canvas',
73
- :deploy => {
74
- :username => 'deploy'
75
- }
76
- }
77
- spec = ConnectionSpecification.new(*spec_args(conf.dup, 'adapter'))
78
- expect(spec.config[:username]).to eq('canvas')
79
- spec.config[:schema_search_path] = 'bob'
80
- expect(spec.config[:schema_search_path]).to eq('bob')
81
- expect(spec.config[:username]).to eq('bob')
82
- GuardRail.activate(:deploy) do
83
- expect(spec.config[:schema_search_path]).to eq('bob')
84
- expect(spec.config[:username]).to eq('deploy')
85
- end
86
- external_config = spec.config.dup
87
- expect(external_config.class).to eq(Hash)
88
- expect(external_config).to eq(spec.config)
89
-
90
- spec.config = conf.dup
91
- expect(spec.config[:username]).to eq('canvas')
92
- end
93
-
94
- it "does not share config objects when dup'ing specs" do
95
- conf = {
96
- :adapter => 'postgresql',
97
- :database => 'primary',
98
- :username => '%{schema_search_path}',
99
- :schema_search_path => 'canvas',
100
- :deploy => {
101
- :username => 'deploy'
102
- }
103
- }
104
- spec = ConnectionSpecification.new(*spec_args(conf.dup, 'adapter'))
105
- expect(spec.config.object_id).not_to eq spec.dup.config.object_id
106
- end
107
-
108
- describe "activate" do
109
- before do
110
- #!!! trick it in to actually switching envs
111
- Rails.env.stubs(:test?).returns(false)
112
-
113
- # be sure to test bugs where the current env isn't yet included in this hash
114
- GuardRail.connection_handlers.clear
115
-
116
- ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
117
- end
118
-
119
- it "should call ensure_handler when switching envs" do
120
- old_handler = ActiveRecord::Base.connection_handler
121
- GuardRail.expects(:ensure_handler).returns(old_handler).twice
122
- GuardRail.activate(:replica) {}
123
- end
124
-
125
- it "should not close connections when switching envs" do
126
- conn = ActiveRecord::Base.connection
127
- replica_conn = GuardRail.activate(:replica) { ActiveRecord::Base.connection }
128
- expect(conn).not_to eq(replica_conn)
129
- expect(ActiveRecord::Base.connection).to eq(conn)
130
- end
131
-
132
- it "should track all activated environments" do
133
- GuardRail.activate(:replica) {}
134
- GuardRail.activate(:custom) {}
135
- expected = Set.new([:primary, :replica, :custom])
136
- expect(GuardRail.activated_environments & expected).to eq(expected)
137
- end
138
-
139
- context "non-transactional" do
140
- it "should really disconnect all envs" do
141
- ActiveRecord::Base.connection
142
- expect(ActiveRecord::Base.connection_pool).to be_connected
143
-
144
- GuardRail.activate(:replica) do
145
- ActiveRecord::Base.connection
146
- expect(ActiveRecord::Base.connection_pool).to be_connected
147
- end
148
-
149
- ActiveRecord::Base.clear_all_connections!
150
- expect(ActiveRecord::Base.connection_pool).not_to be_connected
151
- GuardRail.activate(:replica) do
152
- expect(ActiveRecord::Base.connection_pool).not_to be_connected
153
- end
154
- end
155
- end
156
-
157
- it 'is thread safe' do
158
- GuardRail.activate(:replica) do
159
- Thread.new do
160
- GuardRail.activate!(:deploy)
161
- expect(GuardRail.environment).to eq :deploy
162
- end.join
163
- expect(GuardRail.environment).to eq :replica
164
- end
165
- end
166
- end
167
- end