shackles 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: