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 +20 -0
- data/README.md +51 -0
- data/lib/shackles/connection_handler.rb +15 -0
- data/lib/shackles/connection_specification.rb +53 -0
- data/lib/shackles/railtie.rb +9 -0
- data/lib/shackles/version.rb +3 -0
- data/lib/shackles.rb +96 -0
- data/spec/shackles_spec.rb +125 -0
- metadata +119 -0
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
|
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:
|