shackles 1.0.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.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 Instructure, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,51 @@
1
+ Shackles
2
+ ==========
3
+
4
+ ## About
5
+
6
+ Shackles allows multiple database environments and environment overrides to
7
+ ActiveRecord, allowing least-privilege best practices.
8
+
9
+ ## Installation
10
+
11
+ Add `gem 'shackles'` to your Gemfile (tested with Rails 2.3 and 3.2)
12
+
13
+ ## Usage
14
+
15
+ There are two major use cases for shackles. The first is for master/slave(/deploy) environments.
16
+ Using a slave is as simple as adding a slave block (underneath your main environment block) in
17
+ database.yml, then wrapping stuff you want to query the slave in Shackles.activate(:slave) blocks.
18
+ You can extend this to a deploy environment so that migrations will run as a deploy user that
19
+ permission to modify schema, while your normal app runs with lower privileges that cannot modify
20
+ schema. This is defense-in-depth in practice, so that *if* you happen to have a SQL injection
21
+ bug, it would be impossible to do something like dropping tables.
22
+
23
+ The other major use case is more defense-in-depth. By carefully setting up your environment, you
24
+ can default to script/console sessions for regular users to use their own database user, and the
25
+ slave.
26
+
27
+ Example database.yml file:
28
+
29
+ ```yaml
30
+ production:
31
+ adapter: postgresql
32
+ username: myapp
33
+ database: myapp
34
+ host: db-master
35
+ slave:
36
+ host: db-slave
37
+ deploy:
38
+ username: deploy
39
+ ```
40
+
41
+ Using an initializer, you can achieve the default environment settings (in tandem with profile
42
+ changes):
43
+
44
+ ```ruby
45
+ if ENV['RAILS_DATABASE_ENVIRONMENT']
46
+ Shackles.activate!(ENV['RAILS_DATABASE_ENVIRONMENT'].to_sym)
47
+ end
48
+ if ENV['RAILS_DATABASE_USER']
49
+ Shackles.apply_config!(:username => ENV['RAILS_DATABASE_USER'])
50
+ end
51
+ ```
@@ -0,0 +1,15 @@
1
+ module Shackles
2
+ module ConnectionHandler
3
+ def self.included(klass)
4
+ %w{clear_active_connections clear_reloadable_connections
5
+ clear_all_connections verify_active_connections }.each do |method|
6
+ klass.class_eval(<<EOS)
7
+ def #{method}_with_multiple_environments!
8
+ ::Shackles.connection_handlers.values.each(&:#{method}_without_multiple_environments!)
9
+ end
10
+ EOS
11
+ klass.alias_method_chain "#{method}!".to_sym, :multiple_environments
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,53 @@
1
+ module Shackles
2
+ module ConnectionSpecification
3
+ class CacheCoherentHash < Hash
4
+ def initialize(spec)
5
+ @spec = spec
6
+ super
7
+ end
8
+
9
+ def []=(key, value)
10
+ @spec.instance_variable_set(:@current_config, nil)
11
+ @spec.instance_variable_get(:@config)[key] = value
12
+ end
13
+
14
+ def delete(key)
15
+ @spec.instance_variable_set(:@current_config, nil)
16
+ @spec.instance_variable_get(:@config).delete(key)
17
+ end
18
+ end
19
+
20
+ def self.included(klass)
21
+ klass.send(:remove_method, :config)
22
+ end
23
+
24
+ def config
25
+ @current_config = nil if Shackles.environment != @current_config_environment || Shackles.global_config_sequence != @current_config_sequence
26
+ return @current_config if @current_config
27
+
28
+ @current_config_environment = Shackles.environment
29
+ @current_config_sequence = Shackles.global_config_sequence
30
+ config = @config.dup
31
+ if @config.has_key?(Shackles.environment)
32
+ config.merge!(@config[Shackles.environment].symbolize_keys)
33
+ end
34
+
35
+ config.keys.each do |key|
36
+ next unless config[key].is_a?(String)
37
+ config[key] = config[key] % config
38
+ end
39
+
40
+ config.merge!(Shackles.global_config)
41
+
42
+ @current_config = CacheCoherentHash.new(self)
43
+ @current_config.replace(config)
44
+
45
+ @current_config
46
+ end
47
+
48
+ def config=(value)
49
+ @config = value
50
+ @current_config = nil
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,9 @@
1
+ module Shackles
2
+ class Railtie < Rails::Railtie
3
+ initializer "shackles.extend_ar", :before => "active_record.initialize_database" do
4
+ ActiveSupport.on_load(:active_record) do
5
+ Shackles.initialize!
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module Shackles
2
+ VERSION = "1.0.0"
3
+ end
data/lib/shackles.rb ADDED
@@ -0,0 +1,96 @@
1
+ module Shackles
2
+ class << self
3
+ def environment
4
+ @environment ||= :master
5
+ end
6
+
7
+ def global_config
8
+ @global_config ||= {}
9
+ end
10
+
11
+ # semi-private
12
+ def initialize!
13
+ require 'shackles/connection_handler'
14
+ require 'shackles/connection_specification'
15
+
16
+ ActiveRecord::ConnectionAdapters::ConnectionHandler.send(:include, Shackles::ConnectionHandler)
17
+ ActiveRecord::Base::ConnectionSpecification.send(:include, Shackles::ConnectionSpecification)
18
+ end
19
+
20
+ def global_config_sequence
21
+ @global_config_sequence ||= 1
22
+ end
23
+
24
+ def bump_sequence
25
+ @global_config_sequence ||= 1
26
+ @global_config_sequence += 1
27
+ ActiveRecord::Base::connection_handler.clear_all_connections!
28
+ end
29
+
30
+ # for altering other pieces of config (i.e. username)
31
+ # will force a disconnect
32
+ def apply_config!(hash)
33
+ global_config.merge!(hash)
34
+ bump_sequence
35
+ end
36
+
37
+ def remove_config!(key)
38
+ global_config.delete(key)
39
+ bump_sequence
40
+ end
41
+
42
+ def connection_handlers
43
+ save_handler
44
+ @connection_handlers
45
+ end
46
+
47
+ # switch environment for the duration of the block
48
+ # will keep the old connections around
49
+ def activate(environment)
50
+ environment ||= :master
51
+ return yield if environment == self.environment
52
+ begin
53
+ old_environment = activate!(environment)
54
+ yield
55
+ ensure
56
+ @environment = old_environment
57
+ ActiveRecord::Base.connection_handler = ensure_handler unless Rails.env.test?
58
+ end
59
+ end
60
+
61
+ # for use from script/console ONLY
62
+ def activate!(environment)
63
+ environment ||= :master
64
+ save_handler
65
+ old_environment = self.environment
66
+ @environment = environment
67
+ ActiveRecord::Base.connection_handler = ensure_handler unless Rails.env.test?
68
+ old_environment
69
+ end
70
+
71
+ private
72
+ def save_handler
73
+ @connection_handlers ||= {}
74
+ @connection_handlers[environment] ||= ActiveRecord::Base.connection_handler
75
+ end
76
+
77
+ def ensure_handler
78
+ new_handler = @connection_handlers[environment]
79
+ if !new_handler
80
+ new_handler = @connection_handlers[environment] = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
81
+ variable = Rails.version < '3.0' ? :@connection_pools : :@class_to_pool
82
+ ActiveRecord::Base.connection_handler.instance_variable_get(variable).each do |model, pool|
83
+ new_handler.establish_connection(model, pool.spec)
84
+ end
85
+ end
86
+ new_handler
87
+ end
88
+ end
89
+ end
90
+
91
+ if defined?(Rails::Railtie)
92
+ require "shackles/railtie"
93
+ else
94
+ # just load everything immediately for Rails 2
95
+ Shackles.initialize!
96
+ end
@@ -0,0 +1,125 @@
1
+ require 'active_record'
2
+ require 'rails'
3
+ require 'shackles'
4
+
5
+ # we're not actually bringing up ActiveRecord, so we need to initialize our stuff
6
+ Shackles.initialize!
7
+
8
+ RSpec.configure do |config|
9
+ config.mock_framework = :mocha
10
+ end
11
+
12
+ describe Shackles do
13
+ it "should allow changing environments" do
14
+ conf = {
15
+ :adapter => 'postgresql',
16
+ :database => 'master',
17
+ :username => 'canvas',
18
+ :deploy => {
19
+ :username => 'deploy'
20
+ },
21
+ :slave => {
22
+ :database => 'slave'
23
+ }
24
+ }
25
+ spec = ActiveRecord::Base::ConnectionSpecification.new(conf, 'adapter')
26
+ spec.config[:username].should == 'canvas'
27
+ spec.config[:database].should == 'master'
28
+ Shackles.activate(:deploy) do
29
+ spec.config[:username].should == 'deploy'
30
+ spec.config[:database].should == 'master'
31
+ end
32
+ spec.config[:username].should == 'canvas'
33
+ spec.config[:database].should == 'master'
34
+ Shackles.activate(:slave) do
35
+ spec.config[:username].should == 'canvas'
36
+ spec.config[:database].should == 'slave'
37
+ end
38
+ spec.config[:username].should == 'canvas'
39
+ spec.config[:database].should == 'master'
40
+ end
41
+
42
+ it "should allow using hash insertions" do
43
+ conf = {
44
+ :adapter => 'postgresql',
45
+ :database => 'master',
46
+ :username => '%{schema_search_path}',
47
+ :schema_search_path => 'canvas',
48
+ :deploy => {
49
+ :username => 'deploy'
50
+ }
51
+ }
52
+ spec = ActiveRecord::Base::ConnectionSpecification.new(conf, 'adapter')
53
+ spec.config[:username].should == 'canvas'
54
+ Shackles.activate(:deploy) do
55
+ spec.config[:username].should == 'deploy'
56
+ end
57
+ spec.config[:username].should == 'canvas'
58
+ end
59
+
60
+ it "should be cache coherent with modifying the config" do
61
+ conf = {
62
+ :adapter => 'postgresql',
63
+ :database => 'master',
64
+ :username => '%{schema_search_path}',
65
+ :schema_search_path => 'canvas',
66
+ :deploy => {
67
+ :username => 'deploy'
68
+ }
69
+ }
70
+ spec = ActiveRecord::Base::ConnectionSpecification.new(conf.dup, 'adapter')
71
+ spec.config[:username].should == 'canvas'
72
+ spec.config[:schema_search_path] = 'bob'
73
+ spec.config[:username].should == 'bob'
74
+ Shackles.activate(:deploy) do
75
+ spec.config[:schema_search_path].should == 'bob'
76
+ spec.config[:username].should == 'deploy'
77
+ end
78
+
79
+ spec.config = conf.dup
80
+ spec.config[:username].should == 'canvas'
81
+ end
82
+
83
+ describe "activate" do
84
+ before do
85
+ #!!! trick it in to actually switching envs
86
+ Rails.env.stubs(:test?).returns(false)
87
+
88
+ # be sure to test bugs where the current env isn't yet included in this hash
89
+ Shackles.connection_handlers.clear
90
+
91
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
92
+ end
93
+
94
+ it "should call ensure_handler when switching envs" do
95
+ old_handler = ActiveRecord::Base.connection_handler
96
+ Shackles.expects(:ensure_handler).returns(old_handler).twice
97
+ Shackles.activate(:slave) {}
98
+ end
99
+
100
+ it "should not close connections when switching envs" do
101
+ conn = ActiveRecord::Base.connection
102
+ slave_conn = Shackles.activate(:slave) { ActiveRecord::Base.connection }
103
+ conn.should_not == slave_conn
104
+ ActiveRecord::Base.connection.should == conn
105
+ end
106
+
107
+ context "non-transactional" do
108
+ it "should really disconnect all envs" do
109
+ ActiveRecord::Base.connection
110
+ ActiveRecord::Base.connection_pool.should be_connected
111
+
112
+ Shackles.activate(:slave) do
113
+ ActiveRecord::Base.connection
114
+ ActiveRecord::Base.connection_pool.should be_connected
115
+ end
116
+
117
+ ActiveRecord::Base.clear_all_connections!
118
+ ActiveRecord::Base.connection_pool.should_not be_connected
119
+ Shackles.activate(:slave) do
120
+ ActiveRecord::Base.connection_pool.should_not be_connected
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shackles
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Cody Cutrer
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-04-09 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '2.3'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '2.3'
30
+ - !ruby/object:Gem::Dependency
31
+ name: mocha
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec-core
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '2.13'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '2.13'
62
+ - !ruby/object:Gem::Dependency
63
+ name: sqlite3
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ description: Allows multiple environments in database.yml, and dynamically switching
79
+ them.
80
+ email: cody@instructure.com
81
+ executables: []
82
+ extensions: []
83
+ extra_rdoc_files: []
84
+ files:
85
+ - lib/shackles/connection_handler.rb
86
+ - lib/shackles/connection_specification.rb
87
+ - lib/shackles/railtie.rb
88
+ - lib/shackles/version.rb
89
+ - lib/shackles.rb
90
+ - LICENSE
91
+ - README.md
92
+ - spec/shackles_spec.rb
93
+ homepage: http://github.com/instructure/shackles
94
+ licenses: []
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ none: false
101
+ requirements:
102
+ - - ! '>='
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ! '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubyforge_project:
113
+ rubygems_version: 1.8.23
114
+ signing_key:
115
+ specification_version: 3
116
+ summary: ActiveRecord database environment switching for slaves and least-privilege
117
+ test_files:
118
+ - spec/shackles_spec.rb
119
+ has_rdoc: