bcdatabase 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/CHANGELOG.markdown +29 -0
- data/LICENSE +20 -0
- data/README.markdown +142 -0
- data/Rakefile +53 -0
- data/VERSION.yml +5 -0
- data/bin/bcdatabase +25 -0
- data/lib/bcdatabase/commands.rb +238 -0
- data/lib/bcdatabase.rb +116 -0
- data/spec/bcdatabase/commands_spec.rb +55 -0
- data/spec/bcdatabase_spec.rb +200 -0
- data/spec/spec_helper.rb +32 -0
- metadata +98 -0
data/.gitignore
ADDED
data/CHANGELOG.markdown
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
1.0.0
|
2
|
+
=====
|
3
|
+
- Split out from NUBIC internal `bcdatabase` project.
|
4
|
+
(Changelog entries below reflect the relevant changes & version numbers from that project.)
|
5
|
+
|
6
|
+
0.4.1
|
7
|
+
=====
|
8
|
+
- Fix `bcdatabase encrypt` so that it doesn't re-encrypt already encrypted
|
9
|
+
epassword entries.
|
10
|
+
|
11
|
+
0.4.0
|
12
|
+
=====
|
13
|
+
- Use the YAML entry name as the "database" value if no other value is
|
14
|
+
provided. This is to DRY up PostgreSQL configurations where the username
|
15
|
+
(already defaulted) and the database name are the same.
|
16
|
+
|
17
|
+
0.2.0
|
18
|
+
=====
|
19
|
+
- Change default encrypted secret password location
|
20
|
+
|
21
|
+
0.1.0
|
22
|
+
=====
|
23
|
+
- Support encrypted passwords
|
24
|
+
- Command-line utility (also called bcdatabase) for creating encrypted passwords
|
25
|
+
- Gem distribution
|
26
|
+
|
27
|
+
0.0.0
|
28
|
+
=====
|
29
|
+
Original release.
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Rhett Sutphin
|
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.markdown
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
bcdatabase
|
2
|
+
==========
|
3
|
+
|
4
|
+
*bcdatabase* is a library and utility which provides database configuration parameter management for Ruby on Rails applications. It provides a simple mechanism for separating database configuration attributes from application source code so that there's no temptation to check passwords into the version control system. And it centralizes the parameters for a single server so that they can be easily shared among multiple applications and easily updated by a single administrator.
|
5
|
+
|
6
|
+
## Installing bcdatabase
|
7
|
+
|
8
|
+
Ensure that [gemcutter](http://gemcutter.org) is in your gem sources list, then:
|
9
|
+
|
10
|
+
$ gem install bcdatabase
|
11
|
+
|
12
|
+
## Using bcdatabase to configure the database for a Rails application
|
13
|
+
|
14
|
+
A bog-standard rails application's `config/database.yml` file looks like this:
|
15
|
+
|
16
|
+
development:
|
17
|
+
adapter: oracle-enhanced
|
18
|
+
database: //localhost/XE
|
19
|
+
username: cfg_animal
|
20
|
+
password: not-important
|
21
|
+
|
22
|
+
test:
|
23
|
+
adapter: oracle-enhanced
|
24
|
+
database: //localhost/XE
|
25
|
+
username: cfg_animal_test
|
26
|
+
password: who-cares
|
27
|
+
|
28
|
+
production:
|
29
|
+
adapter: oracle-enhanced
|
30
|
+
database: //super/prod
|
31
|
+
username: cfg_animal
|
32
|
+
password: very-secret
|
33
|
+
|
34
|
+
Rails allows this file to contain [ERB][]. `bcdatabase` uses ERB to replace an entire configuration block. If you wanted to replace, say, just the production block in this example, you would transform it like so:
|
35
|
+
|
36
|
+
<%
|
37
|
+
require 'bcdatabase'
|
38
|
+
bcdb = Bcdatabase.load
|
39
|
+
%>
|
40
|
+
|
41
|
+
development:
|
42
|
+
adapter: oracle-enhanced
|
43
|
+
database: //localhost/XE
|
44
|
+
username: cfg_animal
|
45
|
+
password: not-important
|
46
|
+
|
47
|
+
test:
|
48
|
+
adapter: oracle-enhanced
|
49
|
+
database: //localhost/XE
|
50
|
+
username: cfg_animal_test
|
51
|
+
password: who-cares
|
52
|
+
|
53
|
+
<%= bcdb.production :prod, :cfg_animal %>
|
54
|
+
|
55
|
+
This means "create a YAML block for the *production* environment from the configuration entry named *cfg_animal* in /etc/nubic/db/*prod*.yml." The method called can be anything:
|
56
|
+
|
57
|
+
<%= bcdb.development :local, :cfg_animal %>
|
58
|
+
<%= bcdb.staging 'stage', 'cfg_animal' %>
|
59
|
+
<%= bcdb.automated :dev, :cfg_animal_hudson %>
|
60
|
+
|
61
|
+
[ERB]: http://www.ruby-doc.org/stdlib/libdoc/erb/rdoc/
|
62
|
+
|
63
|
+
## Directly accessing configuration parameters from bcdatabase
|
64
|
+
|
65
|
+
More rarely, you might need to access the actual configuration hash, instead of the YAMLized version. You can access it by invoking `Bcdatabase.load` as shown earlier, then using the bracket operator to specify the configuration you want:
|
66
|
+
|
67
|
+
bcdb[:local, :cfg_animal]
|
68
|
+
|
69
|
+
The resulting hash is suitable for passing to `ActiveRecord::Base.establish_connection`, for instance.
|
70
|
+
|
71
|
+
## Central configuration files
|
72
|
+
|
73
|
+
The database configuration properties for all the applications on a server are stored in one or more files under `/etc/nubic/db` (by default; see "File locations" below). Each one is a standard YAML file, similar to rails' `database.yml` but with a few enhancements:
|
74
|
+
|
75
|
+
* Each file can have a defaults entry which provides attributes which are shared across all configurations in the file
|
76
|
+
* Each entry defaults its "username" attribute to the name of the entry (useful for Oracle)
|
77
|
+
* Each entry defaults its "database" attribute to the name of the entry (useful for PostgreSQL)
|
78
|
+
|
79
|
+
Since each file can define a set of default properties which are shared by all the contained configurations, it makes sense to group databases which have some shared configuration elements.
|
80
|
+
|
81
|
+
### Example
|
82
|
+
|
83
|
+
If you have an `/etc/nubic/db/stage.yml` file that looks like this:
|
84
|
+
|
85
|
+
defaults:
|
86
|
+
adapter: oracle-enhanced
|
87
|
+
database: //mondo/stage
|
88
|
+
cfg_animal:
|
89
|
+
password: secret
|
90
|
+
personnel:
|
91
|
+
username: pers
|
92
|
+
password: more-secret
|
93
|
+
|
94
|
+
You have defined two configuration entries. `:stage, :cfg_animal`:
|
95
|
+
|
96
|
+
adapter: oracle-enhanced
|
97
|
+
username: cfg_animal
|
98
|
+
password: secret
|
99
|
+
database: //mondo/stage
|
100
|
+
|
101
|
+
and `:bcstage, :personnel`:
|
102
|
+
|
103
|
+
adapter: oracle-enhanced
|
104
|
+
username: pers
|
105
|
+
password: more-secret
|
106
|
+
database: //mondo/stage
|
107
|
+
|
108
|
+
## Obscuring passwords
|
109
|
+
|
110
|
+
bcdatabase supports storing encrypted passwords instead of the plaintext ones shown in the previous example. Encrypted passwords are defined with the key `epassword` instead of `password`. The library will decrypt the `epassword` value and expose it to the calling code (usually rails) unencrypted under the `password` key. The `bcdatabase` command line utility handles encrypting passwords; see the next section.
|
111
|
+
|
112
|
+
While the passwords are technically encrypted, the master key must be stored on the same machine so that they can be decrypted on demand. That means this feature only obscures passwords — it will not deter a determined attacker.
|
113
|
+
|
114
|
+
## `bcdatabase` command line utility
|
115
|
+
|
116
|
+
The gem includes a command line utility (also called `bcdatabase`) which assists with creating `epassword` entries. It has online help; after installing the gem, try `bcdatabase help` to read it:
|
117
|
+
|
118
|
+
$ bcdatabase help
|
119
|
+
usage: bcdatabase <command> [args]
|
120
|
+
Command-line utility for bcdatabase 1.0.0
|
121
|
+
encrypt Encrypts all the password entries in a bcdatabase YAML file
|
122
|
+
epass Generate epasswords from individual database passwords
|
123
|
+
gen-key Generate a key for bcdatabase to use
|
124
|
+
help List commands or display help for one
|
125
|
+
|
126
|
+
## File locations
|
127
|
+
|
128
|
+
`/etc/nubic/db` is the default place the library will look for the central configuration files. It may be overridden with the environment variable `BCDATABASE_PATH`. For instance, if you wanted to keep these files in your home directory on your development machine — perhaps so that editing them doesn't require elevated privileges — you could add this to `~/.bashrc`:
|
129
|
+
|
130
|
+
export BCDATABASE_PATH=${HOME}/nubic/db
|
131
|
+
|
132
|
+
Similarly, the file containing the encryption password has a sensible default location, but that location can be overridden by setting `BCDATABASE_PASS`.
|
133
|
+
|
134
|
+
## Credits
|
135
|
+
|
136
|
+
`bcdatabase` was developed at and for the [Northwestern University Biomedical Informatics Center][NUBIC].
|
137
|
+
|
138
|
+
[NUBIC]: http://www.nucats.northwestern.edu/centers/nubic/index.html
|
139
|
+
|
140
|
+
### Copyright
|
141
|
+
|
142
|
+
Copyright (c) 2009 Rhett Sutphin. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "bcdatabase"
|
8
|
+
gem.summary = %Q{Server-central database configuration for rails and other ruby apps}
|
9
|
+
gem.description = %Q{bcdatabase is a tool for storing passwords and other database configuration information outside of your application source tree.}
|
10
|
+
gem.email = "rhett@detailedbalance.net"
|
11
|
+
gem.homepage = "http://github.com/rsutphin/bcdatabase"
|
12
|
+
gem.authors = ["Rhett Sutphin"]
|
13
|
+
gem.add_development_dependency 'rspec', ">= 1.2"
|
14
|
+
gem.add_dependency 'highline', '>= 1.4'
|
15
|
+
gem.add_dependency 'activesupport', '>= 2.0'
|
16
|
+
end
|
17
|
+
Jeweler::GemcutterTasks.new
|
18
|
+
rescue LoadError
|
19
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
20
|
+
end
|
21
|
+
|
22
|
+
require 'spec/rake/spectask'
|
23
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
24
|
+
spec.libs << 'lib' << 'spec'
|
25
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
26
|
+
end
|
27
|
+
|
28
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
29
|
+
spec.libs << 'lib' << 'spec'
|
30
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
31
|
+
spec.rcov = true
|
32
|
+
# rcov can't tell that /Library/Ruby is a system path
|
33
|
+
spec.rcov_opts = ['--exclude', "spec/*,/Library/Ruby/*"]
|
34
|
+
end
|
35
|
+
|
36
|
+
task :spec => :check_dependencies
|
37
|
+
|
38
|
+
task :default => :spec
|
39
|
+
|
40
|
+
require 'rake/rdoctask'
|
41
|
+
Rake::RDocTask.new do |rdoc|
|
42
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
43
|
+
|
44
|
+
rdoc.rdoc_dir = 'rdoc'
|
45
|
+
rdoc.title = "schema_qualified_tables #{version}"
|
46
|
+
rdoc.rdoc_files.include('README*')
|
47
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
48
|
+
end
|
49
|
+
|
50
|
+
# Disable github release since I don't want to commit the gemspec
|
51
|
+
Rake::Task[:release].prerequisites.delete 'github:release'
|
52
|
+
|
53
|
+
task :build => [:gemspec]
|
data/VERSION.yml
ADDED
data/bin/bcdatabase
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/env ruby -W
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bcdatabase/commands'
|
5
|
+
|
6
|
+
module Bcdatabase::Commands
|
7
|
+
UTILITY_NAME = File.basename(__FILE__)
|
8
|
+
end
|
9
|
+
|
10
|
+
###### MAIN
|
11
|
+
|
12
|
+
command = ARGV.shift
|
13
|
+
unless command
|
14
|
+
$stderr.puts "Please specify a command."
|
15
|
+
$stderr.puts Bcdatabase::Commands.help
|
16
|
+
exit(1)
|
17
|
+
end
|
18
|
+
|
19
|
+
klass = Bcdatabase::Commands[command]
|
20
|
+
unless klass
|
21
|
+
$stderr.puts "Unknown command #{command}."
|
22
|
+
$stderr.puts Bcdatabase::Commands.help
|
23
|
+
exit(2)
|
24
|
+
end
|
25
|
+
klass.new(ARGV).main
|
@@ -0,0 +1,238 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'highline'
|
4
|
+
require 'active_support'
|
5
|
+
require 'bcdatabase'
|
6
|
+
|
7
|
+
HL = HighLine.new
|
8
|
+
|
9
|
+
module Bcdatabase::Commands
|
10
|
+
class Base
|
11
|
+
protected
|
12
|
+
|
13
|
+
def self.usage(use)
|
14
|
+
"usage: #{UTILITY_NAME} #{use}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.help_message(use)
|
18
|
+
msg = [ "#{command_name}: #{summary}", usage(use), "" ]
|
19
|
+
yield msg if block_given?
|
20
|
+
msg.join("\n")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Epass < Base
|
25
|
+
def initialize(argv)
|
26
|
+
@streaming = argv[-1] == '-'
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.summary
|
30
|
+
"Generate epasswords from individual database passwords"
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.help
|
34
|
+
help_message("epass [-]") do |msg|
|
35
|
+
msg << "With no arguments, interactively prompts for passwords and"
|
36
|
+
msg << " prints the corresponding epassword entry."
|
37
|
+
msg << ""
|
38
|
+
msg << "If the last argument is -, reads a newline-separated list"
|
39
|
+
msg << " of passwords from standard in and prints the corresponding "
|
40
|
+
msg << " epasswords to standard out."
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def main
|
45
|
+
@streaming ? streamed : interactive
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def streamed
|
51
|
+
$stdin.readlines.each do |line|
|
52
|
+
puts Bcdatabase.encrypt(line.chomp)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def interactive
|
57
|
+
begin
|
58
|
+
loop do
|
59
|
+
pass = HL.ask("Password (^C to end): ") do |q|
|
60
|
+
q.echo = false
|
61
|
+
end
|
62
|
+
puts " epassword: #{Bcdatabase.encrypt(pass)}"
|
63
|
+
end
|
64
|
+
rescue Interrupt
|
65
|
+
puts "\nQuit"
|
66
|
+
exit(0)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class Encrypt < Base
|
72
|
+
def initialize(argv)
|
73
|
+
@input = argv.shift
|
74
|
+
@output = argv.shift
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.summary
|
78
|
+
"Encrypts all the password entries in a bcdatabase YAML file"
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.help
|
82
|
+
help_message("encrypt [inputfile [outputfile]]") do |msg|
|
83
|
+
msg << "Specifically, this command finds all the keys named 'password'"
|
84
|
+
msg << " in the input YAML and substitutes appropriate 'epassword'"
|
85
|
+
msg << " keys."
|
86
|
+
msg << ""
|
87
|
+
msg << "If inputfile is specified, the source will be that file."
|
88
|
+
msg << " If not, the source will be standard in."
|
89
|
+
msg << ""
|
90
|
+
msg << "If inputfile and outputfile are specified, the new file"
|
91
|
+
msg << " will be written to the output file. Otherwise the output"
|
92
|
+
msg << " will go to standard out. Input and output may be the same"
|
93
|
+
msg << " file."
|
94
|
+
msg << ""
|
95
|
+
msg << "You can't read from standard in and write to a file directly; "
|
96
|
+
msg << " use shell file redirection if you need to do that."
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def main
|
101
|
+
inio =
|
102
|
+
if @input
|
103
|
+
open(@input, "r")
|
104
|
+
else
|
105
|
+
$stdin
|
106
|
+
end
|
107
|
+
# try to preserve the order by replacing everything using regexes
|
108
|
+
contents = inio.read
|
109
|
+
contents.gsub!(/\bpassword:(\s*)(\S+)\s*?$/) { "epassword:#{$1}#{Bcdatabase.encrypt($2)}" }
|
110
|
+
outio =
|
111
|
+
if @output
|
112
|
+
open(@output, "w")
|
113
|
+
else
|
114
|
+
$stdout
|
115
|
+
end
|
116
|
+
outio.write(contents)
|
117
|
+
outio.close
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class Help < Base
|
122
|
+
def initialize(argv)
|
123
|
+
@cmd = argv.shift
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.summary
|
127
|
+
"List commands or display help for one; e.g. #{UTILITY_NAME} help epass"
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.help
|
131
|
+
help_message "help [command name]"
|
132
|
+
end
|
133
|
+
|
134
|
+
def main
|
135
|
+
if @cmd
|
136
|
+
klass = Bcdatabase::Commands[@cmd]
|
137
|
+
if klass
|
138
|
+
msg = klass.respond_to?(:help) ? klass.help : klass.summary
|
139
|
+
$stderr.puts msg
|
140
|
+
exit(0)
|
141
|
+
else
|
142
|
+
$stderr.puts "Unknown command #{@cmd}"
|
143
|
+
exit(1)
|
144
|
+
end
|
145
|
+
else
|
146
|
+
$stderr.puts Bcdatabase::Commands.help
|
147
|
+
exit(0)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
class GenKey < Base
|
153
|
+
def initialize(argv)
|
154
|
+
@stream = argv[-1] == '-'
|
155
|
+
end
|
156
|
+
|
157
|
+
def self.summary
|
158
|
+
"Generate a key for bcdatabase to use"
|
159
|
+
end
|
160
|
+
|
161
|
+
def self.help
|
162
|
+
help_message("gen-key [-]") do |msg|
|
163
|
+
msg << "By default, the key will be generated in "
|
164
|
+
msg << " #{Bcdatabase.pass_file}. If the last argument to this"
|
165
|
+
msg << " command is -, the key will be generated to standard out"
|
166
|
+
msg << " instead."
|
167
|
+
msg << ""
|
168
|
+
msg << "CAUTION: writing to #{Bcdatabase.pass_file} may overwrite"
|
169
|
+
msg << " an existing bcdatabase key. If that happens, you will"
|
170
|
+
msg << " need to reencrypt all the epasswords on this machine."
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def main
|
175
|
+
key = random_key(128)
|
176
|
+
outio =
|
177
|
+
if @stream
|
178
|
+
$stdout
|
179
|
+
else
|
180
|
+
file = Bcdatabase.pass_file
|
181
|
+
if File.exist?(file)
|
182
|
+
sure = HL.ask("This operation will overwrite the existing pass file.\n Are you sure you want to do that? ", %w{yes no}) do |q|
|
183
|
+
q.case = :down
|
184
|
+
end
|
185
|
+
unless sure == 'yes'
|
186
|
+
exit(0)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
open(file, "w")
|
190
|
+
end
|
191
|
+
outio.write key
|
192
|
+
outio.close
|
193
|
+
end
|
194
|
+
|
195
|
+
private
|
196
|
+
|
197
|
+
def random_key(length)
|
198
|
+
k = ""
|
199
|
+
# This is probably not going to work in ruby 1.9
|
200
|
+
until k.size == length; k << rand(126 - 32) + 32; end
|
201
|
+
k
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
class << self
|
206
|
+
def help
|
207
|
+
all_help = commands.collect { |c| [c.command_name, c.summary] }.sort_by { |p| p[0] }
|
208
|
+
max_name_length = all_help.collect { |a| a[0].size }.max
|
209
|
+
msg = Base.usage "<command> [args]\n"
|
210
|
+
msg << "Utility for bcdatabase #{Bcdatabase::VERSION}\n"
|
211
|
+
msg << "Commands:\n"
|
212
|
+
msg << all_help.collect { |name, help| " %#{max_name_length + 1}s %s" % [name, help] }.join("\n")
|
213
|
+
end
|
214
|
+
|
215
|
+
# Lists all the commands
|
216
|
+
def commands
|
217
|
+
constants.reject { |cs| cs == "Base" }.collect { |cs| const_get(cs) }.select { |c| c.kind_of? Class }
|
218
|
+
end
|
219
|
+
|
220
|
+
# Locates the command class for a user-entered command name.
|
221
|
+
# Returns nil if the name is invalid.
|
222
|
+
def command(command_name)
|
223
|
+
begin
|
224
|
+
klassname = command_name.gsub('-', '_').camelize
|
225
|
+
Bcdatabase::Commands.const_get "#{klassname}"
|
226
|
+
rescue NameError
|
227
|
+
nil
|
228
|
+
end
|
229
|
+
end
|
230
|
+
alias :[] :command
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
class Class
|
235
|
+
def command_name
|
236
|
+
name.gsub(Bcdatabase::Commands.name + "::", '').underscore.gsub("_", '-')
|
237
|
+
end
|
238
|
+
end
|
data/lib/bcdatabase.rb
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'openssl'
|
3
|
+
require 'digest/sha2'
|
4
|
+
require 'base64'
|
5
|
+
|
6
|
+
module Bcdatabase
|
7
|
+
VERSION = begin
|
8
|
+
config = YAML.load(File.read(File.expand_path('../VERSION.yml', File.dirname(__FILE__))))
|
9
|
+
[config[:major], config[:minor], config[:patch]].join('.')
|
10
|
+
end
|
11
|
+
DEFAULT_BASE_PATH = File.join('/', 'etc', 'nubic', 'db')
|
12
|
+
DEFAULT_PASS_FILE = File.join('/', 'var', 'lib', 'nubic', 'db.pass')
|
13
|
+
CIPHER = 'aes-256-ecb'
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def load(path=nil)
|
17
|
+
path ||= base_path
|
18
|
+
files = Dir.glob(File.join(path, "*.yml")) + Dir.glob(File.join(path, "*.yaml"))
|
19
|
+
DatabaseConfigurations.new(files)
|
20
|
+
end
|
21
|
+
|
22
|
+
def encrypt(s)
|
23
|
+
Base64.encode64(encipher(:encrypt, s)).strip
|
24
|
+
end
|
25
|
+
|
26
|
+
def decrypt(s)
|
27
|
+
encipher(:decrypt, Base64.decode64(s))
|
28
|
+
end
|
29
|
+
|
30
|
+
def pass_file
|
31
|
+
ENV["BCDATABASE_PASS"] || DEFAULT_PASS_FILE
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# based on http://snippets.dzone.com/posts/show/576
|
37
|
+
def encipher(direction, s)
|
38
|
+
# the order of operations here is very important
|
39
|
+
c = OpenSSL::Cipher::Cipher.new(CIPHER)
|
40
|
+
c.send direction
|
41
|
+
c.key = pass
|
42
|
+
t = c.update(s)
|
43
|
+
t << c.final
|
44
|
+
end
|
45
|
+
|
46
|
+
def pass
|
47
|
+
return @pass if instance_variable_defined? :@pass
|
48
|
+
|
49
|
+
contents = open(pass_file).read.chomp
|
50
|
+
# This code may not work correctly on Ruby 1.9
|
51
|
+
if contents.size == 32
|
52
|
+
@pass = contents
|
53
|
+
else
|
54
|
+
@pass = Digest::SHA256.digest(contents)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def base_path
|
59
|
+
ENV["BCDATABASE_PATH"] || DEFAULT_BASE_PATH
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class DatabaseConfigurations
|
64
|
+
def initialize(files)
|
65
|
+
@files = files
|
66
|
+
@map = { }
|
67
|
+
files.each do |filename|
|
68
|
+
name = File.basename(filename).gsub(/\.ya?ml/, '')
|
69
|
+
@map[name] = YAML.load(File.open(filename))
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def [](groupname, dbname)
|
74
|
+
create_entry(groupname.to_s, dbname.to_s)
|
75
|
+
end
|
76
|
+
|
77
|
+
def method_missing(name, *args)
|
78
|
+
groupname = (args[0] or raise "Database configuration group not specified for #{name}")
|
79
|
+
dbname = (args[1] or raise "Database entry name not specified for #{name}")
|
80
|
+
n = name.to_s
|
81
|
+
begin
|
82
|
+
unseparated_yaml({ n, self[groupname, dbname] })
|
83
|
+
rescue Bcdatabase::Error => e
|
84
|
+
if defined?(RAILS_ENV) and RAILS_ENV == n
|
85
|
+
raise e
|
86
|
+
else
|
87
|
+
# Not using that configuration right now, so return a dummy instead
|
88
|
+
# of throwing an exception
|
89
|
+
unseparated_yaml({ n, { 'error' => e.message } })
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def create_entry(groupname, dbname)
|
97
|
+
group = @map[groupname] or raise Error.new("No databasease configuration group named #{groupname.inspect} found. (Found #{@map.keys.inspect}.)")
|
98
|
+
db = group[dbname] or raise Error.new("No database entry for #{dbname.inspect} in #{groupname}")
|
99
|
+
merged = { 'username' => dbname, 'database' => dbname } \
|
100
|
+
.merge(group['defaults'] || {}) \
|
101
|
+
.merge(group['default'] || {}) \
|
102
|
+
.merge(db)
|
103
|
+
# include the decrypted password if an encrypted one was provided
|
104
|
+
if merged['epassword']
|
105
|
+
merged['password'] = Bcdatabase.decrypt(merged['epassword'])
|
106
|
+
end
|
107
|
+
merged
|
108
|
+
end
|
109
|
+
|
110
|
+
def unseparated_yaml(arg)
|
111
|
+
arg.to_yaml.gsub(/^---.*\n/, '')
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class Error < Exception; end
|
116
|
+
end unless defined?(Bcdatabase)
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require File.expand_path("../spec_helper", File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require "bcdatabase/commands"
|
4
|
+
|
5
|
+
describe "CLI: bcdatabase" do
|
6
|
+
before(:each) do
|
7
|
+
ENV["BCDATABASE_PATH"] = "/tmp/bcdb_specs"
|
8
|
+
FileUtils.mkdir_p ENV["BCDATABASE_PATH"]
|
9
|
+
end
|
10
|
+
|
11
|
+
after(:each) do
|
12
|
+
FileUtils.rm_rf ENV["BCDATABASE_PATH"]
|
13
|
+
ENV["BCDATABASE_PATH"] = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "encrypt" do
|
17
|
+
before do
|
18
|
+
enable_fake_cipherment
|
19
|
+
end
|
20
|
+
|
21
|
+
after do
|
22
|
+
disable_fake_cipherment
|
23
|
+
end
|
24
|
+
|
25
|
+
def bcdatabase_encrypt(infile)
|
26
|
+
StringIO.open("", "w") do |io|
|
27
|
+
$stdout = io
|
28
|
+
Bcdatabase::Commands::Encrypt.new([File.join(ENV["BCDATABASE_PATH"], infile)]).main
|
29
|
+
$stdout = STDOUT
|
30
|
+
YAML::load(io.string)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it "replaces password: clauses with epasswords" do
|
35
|
+
temporary_yaml "plain", {
|
36
|
+
"single" => {
|
37
|
+
"password" => 'zanzibar'
|
38
|
+
}
|
39
|
+
}
|
40
|
+
|
41
|
+
bcdatabase_encrypt('plain.yaml')['single']['epassword'].should == 'rabiznaz'
|
42
|
+
bcdatabase_encrypt('plain.yaml')['single']['password'].should be_nil
|
43
|
+
end
|
44
|
+
|
45
|
+
it "leaves existing epasswords alone" do
|
46
|
+
temporary_yaml "plain", {
|
47
|
+
"single" => {
|
48
|
+
"epassword" => 'etalocohc'
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
bcdatabase_encrypt('plain.yaml')['single']['epassword'].should == 'etalocohc'
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
require File.expand_path("spec_helper", File.dirname(__FILE__))
|
2
|
+
|
3
|
+
describe Bcdatabase, "cipherment" do
|
4
|
+
before(:all) do
|
5
|
+
keyfile = "/tmp/bcdb-spec-key"
|
6
|
+
open(keyfile, 'w') { |f| f.write "01234567890123456789012345678901" }
|
7
|
+
ENV["BCDATABASE_PASS"] = keyfile
|
8
|
+
end
|
9
|
+
|
10
|
+
after(:all) do
|
11
|
+
FileUtils.rm ENV["BCDATABASE_PASS"]
|
12
|
+
ENV["BCDATABASE_PASS"] = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should be reversible" do
|
16
|
+
e = Bcdatabase.encrypt("riboflavin")
|
17
|
+
Bcdatabase.decrypt(e).should == "riboflavin"
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should permute the input" do
|
21
|
+
Bcdatabase.encrypt("zanzibar").should_not == "zanzibar"
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should do more than just encode" do
|
25
|
+
Bcdatabase.encrypt("zanzibar").should_not == Base64.encode64("zanzibar")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe Bcdatabase, "module " do
|
30
|
+
it "should have a d.d.d version" do
|
31
|
+
Bcdatabase::VERSION.should =~ /^\d+\.\d+\.\d+$/
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe Bcdatabase, "loading" do
|
36
|
+
before(:each) do
|
37
|
+
ENV["BCDATABASE_PATH"] = "/tmp/bcdb_specs"
|
38
|
+
FileUtils.mkdir_p ENV["BCDATABASE_PATH"]
|
39
|
+
end
|
40
|
+
|
41
|
+
after(:each) do
|
42
|
+
FileUtils.rm_rf ENV["BCDATABASE_PATH"]
|
43
|
+
ENV["BCDATABASE_PATH"] = nil
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should read simple YAML" do
|
47
|
+
temporary_yaml "simple", {
|
48
|
+
"single" => {
|
49
|
+
"adapter" => "foo", "username" => "baz"
|
50
|
+
}
|
51
|
+
}
|
52
|
+
bcdb = Bcdatabase.load
|
53
|
+
bcdb[:simple, :single]['adapter'].should == "foo"
|
54
|
+
bcdb[:simple, :single]['username'].should == "baz"
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should read and expose multiple groups from multiple files" do
|
58
|
+
temporary_yaml "one", {
|
59
|
+
"first" => { "dc" => "etc" }
|
60
|
+
}
|
61
|
+
temporary_yaml "two", {
|
62
|
+
"fourth" => { "dc" => "etc" }
|
63
|
+
}
|
64
|
+
bcdb = Bcdatabase.load
|
65
|
+
bcdb['one', 'first'].should_not be_nil
|
66
|
+
bcdb['two', 'fourth'].should_not be_nil
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should merge defaults from 'defaults'" do
|
70
|
+
temporary_yaml "defaulted", {
|
71
|
+
"defaults" => {
|
72
|
+
"database" => "postgresql"
|
73
|
+
},
|
74
|
+
"real" => {
|
75
|
+
"password" => "frood"
|
76
|
+
}
|
77
|
+
}
|
78
|
+
bcdb = Bcdatabase.load
|
79
|
+
bcdb['defaulted', 'real']['password'].should == 'frood'
|
80
|
+
bcdb['defaulted', 'real']['database'].should == 'postgresql'
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should merge defaults from 'default'" do
|
84
|
+
temporary_yaml "singular", {
|
85
|
+
"default" => {
|
86
|
+
"adapter" => "three-eighths"
|
87
|
+
},
|
88
|
+
"real" => {
|
89
|
+
"password" => "frood"
|
90
|
+
}
|
91
|
+
}
|
92
|
+
bcdb = Bcdatabase.load
|
93
|
+
bcdb['singular', 'real']['adapter'].should == 'three-eighths'
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should preserve values overridden from defaults" do
|
97
|
+
temporary_yaml "jam", {
|
98
|
+
"default" => {
|
99
|
+
"adapter" => "three-eighths"
|
100
|
+
},
|
101
|
+
"standard" => {
|
102
|
+
"password" => "frood"
|
103
|
+
},
|
104
|
+
"custom" => {
|
105
|
+
"adapter" => "five-sixteenths",
|
106
|
+
"password" => "lazlo"
|
107
|
+
}
|
108
|
+
}
|
109
|
+
bcdb = Bcdatabase.load
|
110
|
+
bcdb['jam', 'standard']['adapter'].should == 'three-eighths'
|
111
|
+
bcdb['jam', 'custom']['adapter'].should == 'five-sixteenths'
|
112
|
+
end
|
113
|
+
|
114
|
+
it "should default the username to the entry name" do
|
115
|
+
temporary_yaml "scran", {
|
116
|
+
"jim" => { "password" => "leather" }
|
117
|
+
}
|
118
|
+
bcdb = Bcdatabase.load
|
119
|
+
bcdb['scran', 'jim']['username'].should == 'jim'
|
120
|
+
bcdb['scran', 'jim']['password'].should == 'leather'
|
121
|
+
end
|
122
|
+
|
123
|
+
it "should default the database name to the entry name" do
|
124
|
+
temporary_yaml "scran", {
|
125
|
+
"jim" => { "password" => "leather" }
|
126
|
+
}
|
127
|
+
bcdb = Bcdatabase.load
|
128
|
+
bcdb['scran', 'jim']['database'].should == 'jim'
|
129
|
+
bcdb['scran', 'jim']['password'].should == 'leather'
|
130
|
+
end
|
131
|
+
|
132
|
+
it "should not default the database name if there's an explicit database name" do
|
133
|
+
temporary_yaml "scran", {
|
134
|
+
"jim" => {
|
135
|
+
"password" => "leather",
|
136
|
+
"database" => "james"
|
137
|
+
}
|
138
|
+
}
|
139
|
+
bcdb = Bcdatabase.load
|
140
|
+
bcdb['scran', 'jim']['database'].should == 'james'
|
141
|
+
bcdb['scran', 'jim']['password'].should == 'leather'
|
142
|
+
end
|
143
|
+
|
144
|
+
it "should not default the database name to the entry name if there's a default database name" do
|
145
|
+
temporary_yaml "scran", {
|
146
|
+
"default" => {
|
147
|
+
"database" => "//localhost:345/etc"
|
148
|
+
},
|
149
|
+
"jim" => {
|
150
|
+
"password" => "leather",
|
151
|
+
}
|
152
|
+
}
|
153
|
+
bcdb = Bcdatabase.load
|
154
|
+
bcdb['scran', 'jim']['database'].should == '//localhost:345/etc'
|
155
|
+
bcdb['scran', 'jim']['password'].should == 'leather'
|
156
|
+
end
|
157
|
+
|
158
|
+
it "should use an explicit username instead of the entry name if provided" do
|
159
|
+
temporary_yaml "scran", {
|
160
|
+
"jim" => {
|
161
|
+
"username" => "james",
|
162
|
+
"password" => "earldom"
|
163
|
+
}
|
164
|
+
}
|
165
|
+
bcdb = Bcdatabase.load
|
166
|
+
bcdb['scran', 'jim']['username'].should == 'james'
|
167
|
+
bcdb['scran', 'jim']['password'].should == 'earldom'
|
168
|
+
end
|
169
|
+
|
170
|
+
describe "with encrypted passwords" do
|
171
|
+
before do
|
172
|
+
enable_fake_cipherment
|
173
|
+
end
|
174
|
+
|
175
|
+
after do
|
176
|
+
disable_fake_cipherment
|
177
|
+
end
|
178
|
+
|
179
|
+
it "should decrypt and expose the password" do
|
180
|
+
temporary_yaml "secure", {
|
181
|
+
"safe" => {
|
182
|
+
"epassword" => "moof"
|
183
|
+
}
|
184
|
+
}
|
185
|
+
bcdb = Bcdatabase.load
|
186
|
+
bcdb['secure', 'safe']['password'].should == "foom"
|
187
|
+
end
|
188
|
+
|
189
|
+
it "should prefer the decrypted version of an epassword" do
|
190
|
+
temporary_yaml "secure", {
|
191
|
+
"safe" => {
|
192
|
+
"password" => "fake",
|
193
|
+
"epassword" => "moof"
|
194
|
+
}
|
195
|
+
}
|
196
|
+
bcdb = Bcdatabase.load
|
197
|
+
bcdb['secure', 'safe']['password'].should == "foom" # not "fake"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
require 'bcdatabase'
|
4
|
+
require 'rubygems'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
def temporary_yaml(name, hash)
|
8
|
+
filename = "/#{ENV['BCDATABASE_PATH']}/#{name}.yaml"
|
9
|
+
open(filename, "w") { |f| YAML.dump(hash, f) }
|
10
|
+
filename
|
11
|
+
end
|
12
|
+
|
13
|
+
def enable_fake_cipherment
|
14
|
+
# replace real encryption methods with something predictable
|
15
|
+
Bcdatabase.module_eval do
|
16
|
+
class << self
|
17
|
+
alias :encrypt_original :encrypt
|
18
|
+
alias :decrypt_original :decrypt
|
19
|
+
def encrypt(s); s.reverse; end
|
20
|
+
def decrypt(s); s.reverse; end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def disable_fake_cipherment
|
26
|
+
Bcdatabase.module_eval do
|
27
|
+
class << self
|
28
|
+
alias :encrypt :encrypt_original
|
29
|
+
alias :decrypt :decrypt_original
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bcdatabase
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rhett Sutphin
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-12-08 00:00:00 -06:00
|
13
|
+
default_executable: bcdatabase
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rspec
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "1.2"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: highline
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "1.4"
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: activesupport
|
37
|
+
type: :runtime
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: "2.0"
|
44
|
+
version:
|
45
|
+
description: bcdatabase is a tool for storing passwords and other database configuration information outside of your application source tree.
|
46
|
+
email: rhett@detailedbalance.net
|
47
|
+
executables:
|
48
|
+
- bcdatabase
|
49
|
+
extensions: []
|
50
|
+
|
51
|
+
extra_rdoc_files:
|
52
|
+
- LICENSE
|
53
|
+
- README.markdown
|
54
|
+
files:
|
55
|
+
- .gitignore
|
56
|
+
- CHANGELOG.markdown
|
57
|
+
- LICENSE
|
58
|
+
- README.markdown
|
59
|
+
- Rakefile
|
60
|
+
- VERSION.yml
|
61
|
+
- bin/bcdatabase
|
62
|
+
- lib/bcdatabase.rb
|
63
|
+
- lib/bcdatabase/commands.rb
|
64
|
+
- spec/bcdatabase/commands_spec.rb
|
65
|
+
- spec/bcdatabase_spec.rb
|
66
|
+
- spec/spec_helper.rb
|
67
|
+
has_rdoc: true
|
68
|
+
homepage: http://github.com/rsutphin/bcdatabase
|
69
|
+
licenses: []
|
70
|
+
|
71
|
+
post_install_message:
|
72
|
+
rdoc_options:
|
73
|
+
- --charset=UTF-8
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: "0"
|
81
|
+
version:
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: "0"
|
87
|
+
version:
|
88
|
+
requirements: []
|
89
|
+
|
90
|
+
rubyforge_project:
|
91
|
+
rubygems_version: 1.3.5
|
92
|
+
signing_key:
|
93
|
+
specification_version: 3
|
94
|
+
summary: Server-central database configuration for rails and other ruby apps
|
95
|
+
test_files:
|
96
|
+
- spec/bcdatabase/commands_spec.rb
|
97
|
+
- spec/bcdatabase_spec.rb
|
98
|
+
- spec/spec_helper.rb
|