conf_conf 1.0.2 → 2.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/README.md +227 -39
- data/bin/conf_conf +136 -0
- data/conf_conf.gemspec +18 -10
- data/lib/conf_conf.rb +51 -42
- data/lib/conf_conf/cli.rb +9 -0
- data/lib/conf_conf/cli/developers.rb +48 -0
- data/lib/conf_conf/cli/environments.rb +61 -0
- data/lib/conf_conf/cli/root.rb +72 -0
- data/lib/conf_conf/cli/variables.rb +85 -0
- data/lib/conf_conf/configuration.rb +57 -0
- data/lib/conf_conf/project.rb +20 -0
- data/lib/conf_conf/project/developer.rb +38 -0
- data/lib/conf_conf/project/developers.rb +48 -0
- data/lib/conf_conf/project/environment.rb +53 -0
- data/lib/conf_conf/project/environment/storage.rb +58 -0
- data/lib/conf_conf/project/environments.rb +24 -0
- data/lib/conf_conf/remote.rb +40 -0
- data/lib/conf_conf/user.rb +38 -0
- data/spec/project/developers_spec.rb +25 -0
- data/spec/project/environment/storage_spec.rb +91 -0
- data/spec/project/environment_spec.rb +21 -0
- data/spec/spec_helper.rb +163 -0
- metadata +126 -9
- data/spec/conf_conf_spec.rb +0 -68
@@ -0,0 +1,20 @@
|
|
1
|
+
module ConfConf
|
2
|
+
class Project
|
3
|
+
attr_reader :developers, :environments
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@developers = Developers.load(self)
|
7
|
+
@environments = Environments.new(self)
|
8
|
+
end
|
9
|
+
|
10
|
+
def inconsistencies(environment)
|
11
|
+
all_environment_variable_names = Set.new
|
12
|
+
|
13
|
+
environments.to_a.each do |other_environment|
|
14
|
+
all_environment_variable_names += other_environment.variables.keys
|
15
|
+
end
|
16
|
+
|
17
|
+
all_environment_variable_names - environment.variables.keys
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'rbnacl'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
module ConfConf
|
5
|
+
class Project
|
6
|
+
class Developer < Struct.new(:pretty_public_key, :pretty_private_key)
|
7
|
+
def self.current
|
8
|
+
user_config_path = File.join(File.expand_path('~'), '.conf_conf.json')
|
9
|
+
|
10
|
+
if File.exists?(user_config_path)
|
11
|
+
user_config = MultiJson.load(File.read(user_config_path))
|
12
|
+
|
13
|
+
else
|
14
|
+
private_key = RbNaCl::PrivateKey.generate
|
15
|
+
pretty_private_key = Base64.strict_encode64(private_key.to_s)
|
16
|
+
pretty_public_key = Base64.strict_encode64(private_key.public_key.to_s)
|
17
|
+
|
18
|
+
user_config = {
|
19
|
+
'public_key' => pretty_public_key,
|
20
|
+
'private_key' => pretty_private_key
|
21
|
+
}
|
22
|
+
|
23
|
+
File.write(user_config_path, MultiJson.dump(user_config))
|
24
|
+
end
|
25
|
+
|
26
|
+
Developer.new(user_config['public_key'], user_config['private_key'])
|
27
|
+
end
|
28
|
+
|
29
|
+
def private_key
|
30
|
+
Base64.decode64(pretty_private_key)
|
31
|
+
end
|
32
|
+
|
33
|
+
def public_key
|
34
|
+
Base64.decode64(pretty_public_key)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module ConfConf
|
2
|
+
class Project
|
3
|
+
class Developers < Struct.new(:project)
|
4
|
+
class << self
|
5
|
+
def load(project)
|
6
|
+
developers = Developers.new(project)
|
7
|
+
|
8
|
+
if File.exists?(Developers.path)
|
9
|
+
developers_json = File.read(Developers.path)
|
10
|
+
developers_keys = MultiJson.load(developers_json)
|
11
|
+
developers.keys = developers_keys
|
12
|
+
end
|
13
|
+
|
14
|
+
developers
|
15
|
+
end
|
16
|
+
|
17
|
+
def path
|
18
|
+
File.join('config', 'conf_conf', 'developers.json')
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def add(developer)
|
23
|
+
keys.add(developer.pretty_public_key).to_a
|
24
|
+
end
|
25
|
+
|
26
|
+
def remove(developer)
|
27
|
+
keys.delete(developer.pretty_public_key).to_a
|
28
|
+
end
|
29
|
+
|
30
|
+
def keys=(keys)
|
31
|
+
@keys = Set.new(keys)
|
32
|
+
end
|
33
|
+
|
34
|
+
def keys
|
35
|
+
@keys ||= Set.new
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_a
|
39
|
+
keys.collect { |key| Developer.new(key) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def save
|
43
|
+
developers_json = MultiJson.dump(keys.to_a)
|
44
|
+
File.write(Developers.path, developers_json)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module ConfConf
|
2
|
+
class Project
|
3
|
+
class Environment < Struct.new(:project, :name, :variables, :schema)
|
4
|
+
class << self
|
5
|
+
def load(project, name)
|
6
|
+
ConfConf::Project::Environment::Storage.load_project_environment(project, name)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(*args)
|
11
|
+
super
|
12
|
+
self.variables ||= {}
|
13
|
+
self.schema ||= {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def get(variable_name)
|
17
|
+
variable_name = normalized_variable_name(variable_name)
|
18
|
+
variables[variable_name]
|
19
|
+
end
|
20
|
+
|
21
|
+
def set(variable_name, variable_value)
|
22
|
+
variable_name = normalized_variable_name(variable_name)
|
23
|
+
|
24
|
+
if variables[variable_name] != variable_value
|
25
|
+
schema.delete variable_name
|
26
|
+
end
|
27
|
+
|
28
|
+
if schema[variable_name] && schema[variable_name]['access']
|
29
|
+
schema[variable_name]['access'] = (project.developers.keys + schema[variable_name]['access']).to_a
|
30
|
+
else
|
31
|
+
schema[variable_name] = { 'access' => project.developers.keys.to_a }
|
32
|
+
end
|
33
|
+
|
34
|
+
variables[variable_name] = variable_value
|
35
|
+
end
|
36
|
+
|
37
|
+
def remove(variable_name)
|
38
|
+
variable_name = normalized_variable_name(variable_name)
|
39
|
+
schema.delete variable_name
|
40
|
+
variables.delete variable_name
|
41
|
+
end
|
42
|
+
|
43
|
+
def save
|
44
|
+
ConfConf::Project::Environment::Storage.save_project_environment(project, self)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
def normalized_variable_name(variable_name)
|
49
|
+
variable_name.strip.upcase
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module ConfConf
|
2
|
+
class Project
|
3
|
+
class Environment
|
4
|
+
class NotAuthorizedError < StandardError; end;
|
5
|
+
|
6
|
+
class Storage
|
7
|
+
def self.load_project_environment(project, environment_name)
|
8
|
+
current_developer = ConfConf::Project::Developer.current
|
9
|
+
environment_config_file_path = File.join('config', 'conf_conf', 'environments', "#{environment_name}.json")
|
10
|
+
|
11
|
+
if File.exists?(environment_config_file_path)
|
12
|
+
environment_config = MultiJson.load(File.read(environment_config_file_path))
|
13
|
+
environment_schema = environment_config['schema']
|
14
|
+
author_public_key = Base64.decode64(environment_config['author_public_key'])
|
15
|
+
encrypted_environment_secret_key = environment_config['encrypted_environment_secret_key'][current_developer.pretty_public_key]
|
16
|
+
|
17
|
+
raise NotAuthorizedError if encrypted_environment_secret_key.nil?
|
18
|
+
|
19
|
+
box = RbNaCl::SimpleBox.from_keypair(author_public_key, current_developer.private_key)
|
20
|
+
environment_secret_key = box.decrypt(encrypted_environment_secret_key)
|
21
|
+
|
22
|
+
box = RbNaCl::SimpleBox.from_secret_key(environment_secret_key)
|
23
|
+
decrypted_environment_variables_json = box.decrypt(environment_config["encrypted_environment"])
|
24
|
+
|
25
|
+
environment_variables = MultiJson.load(decrypted_environment_variables_json)
|
26
|
+
else
|
27
|
+
environment_variables = {}
|
28
|
+
environment_schema = {}
|
29
|
+
end
|
30
|
+
|
31
|
+
Environment.new(project, environment_name, environment_variables, environment_schema)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.save_project_environment(project, environment)
|
35
|
+
author = ConfConf::Project::Developer.current
|
36
|
+
|
37
|
+
environment_config = {}
|
38
|
+
environment_config[:author_public_key] = author.pretty_public_key
|
39
|
+
environment_config[:schema] = environment.schema
|
40
|
+
|
41
|
+
environment_secret_key = RbNaCl::Random.random_bytes(RbNaCl::SecretBox.key_bytes)
|
42
|
+
project.developers.add(author)
|
43
|
+
|
44
|
+
environment_config[:encrypted_environment_secret_key] = project.developers.to_a.inject({}) do |keys,developer|
|
45
|
+
box = RbNaCl::SimpleBox.from_keypair(developer.public_key, author.private_key)
|
46
|
+
keys[developer.pretty_public_key] = box.encrypt(environment_secret_key); keys
|
47
|
+
end
|
48
|
+
|
49
|
+
box = RbNaCl::SimpleBox.from_secret_key(environment_secret_key)
|
50
|
+
environment_config[:encrypted_environment] = box.encrypt(MultiJson.dump(environment.variables))
|
51
|
+
|
52
|
+
environment_config_file_path = File.join('config', 'conf_conf', 'environments', "#{environment.name}.json")
|
53
|
+
File.write(environment_config_file_path, MultiJson.dump(environment_config))
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ConfConf
|
2
|
+
class Project
|
3
|
+
class Environments < Struct.new(:project)
|
4
|
+
def length
|
5
|
+
Dir["config/conf_conf/environments/*.json"].length
|
6
|
+
end
|
7
|
+
|
8
|
+
def remove(name)
|
9
|
+
FileUtils.rm_f("config/conf_conf/environments/#{name}.json")
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_a
|
13
|
+
Dir["config/conf_conf/environments/*.json"].collect do |path|
|
14
|
+
environment_name = File.basename(path, File.extname(path))
|
15
|
+
self[environment_name]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def [](name)
|
20
|
+
Environment.load(project, name)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module ConfConf
|
2
|
+
## Interfaces with the conf_conf.io system
|
3
|
+
module Remote
|
4
|
+
SAVE_ENVIRONMENT_URL = "https://api.conf_conf.io/save"
|
5
|
+
LOAD_ENVIRONMENT_URL = "https://api.conf_conf.io/load"
|
6
|
+
|
7
|
+
def self.save_environment(environment)
|
8
|
+
request = HTTPI::Request.new
|
9
|
+
request.url = SAVE_ENVIRONMENT_URL
|
10
|
+
request.body = {
|
11
|
+
project: environment.project,
|
12
|
+
name: environment.name,
|
13
|
+
encrypted_env_variables: environment.encrypted_env_variables
|
14
|
+
}
|
15
|
+
|
16
|
+
response = HTTPI.post(request)
|
17
|
+
|
18
|
+
raise ConfConf::Remote::SaveException.new(response.body) if response.error?
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.load_environment(project, name)
|
22
|
+
request = HTTPI::Request.new
|
23
|
+
request.url = LOAD_ENVIRONMENT_URL
|
24
|
+
request.body = {
|
25
|
+
project: project,
|
26
|
+
name: name
|
27
|
+
}
|
28
|
+
|
29
|
+
response = HTTPI.post(request)
|
30
|
+
|
31
|
+
raise ConfConf::Remote::LoadException.new(response.body) if response.error?
|
32
|
+
|
33
|
+
parsed_response = MultiJson.load(response.body)
|
34
|
+
|
35
|
+
environment = Environment.new(parsed_response.project, parsed_response.name)
|
36
|
+
environment.encrypted_env_variables = parsed_response.encrypted_env_variables
|
37
|
+
environment
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'rbnacl/libsodium'
|
3
|
+
require 'conf_conf/config'
|
4
|
+
|
5
|
+
module ConfConf
|
6
|
+
class User < Struct.new(:public_key, :private_key)
|
7
|
+
class << self
|
8
|
+
def current
|
9
|
+
config = Config.load
|
10
|
+
User.new(config.public_key, config.private_key)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Config < ConfConf::Config
|
15
|
+
def self.path
|
16
|
+
File.join(File.expand_path('~'), '.conf_conf.json')
|
17
|
+
end
|
18
|
+
|
19
|
+
config_attr :public_key, default: :new_public_key
|
20
|
+
config_attr :private_key, default: :new_private_key
|
21
|
+
|
22
|
+
private
|
23
|
+
def new_public_key
|
24
|
+
generate_keys! and @public_key
|
25
|
+
end
|
26
|
+
|
27
|
+
def new_private_key
|
28
|
+
generate_keys! and @private_key
|
29
|
+
end
|
30
|
+
|
31
|
+
def generate_keys!
|
32
|
+
@key ||= RbNaCl::PrivateKey.generate
|
33
|
+
@private_key = Base64.strict_encode64(@key.to_s)
|
34
|
+
@public_key = Base64.strict_encode64(@key.public_key.to_s)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ConfConf::Project::Developers do
|
4
|
+
let(:developer) { ConfConf::Project::Developer.new(pretty_public_key) }
|
5
|
+
let(:pretty_public_key) { Base64.strict_encode64('hello') }
|
6
|
+
|
7
|
+
before do
|
8
|
+
stub_const('File', MockFile)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'adds a new developer to config/conf_conf/developers.json' do
|
12
|
+
subject.add(developer)
|
13
|
+
subject.save
|
14
|
+
|
15
|
+
expect(subject.keys.length).to eq(1)
|
16
|
+
expect(subject.keys.first).to eq(pretty_public_key)
|
17
|
+
expect(File.exists?('config/conf_conf/developers.json')).to be(true)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'removes a developer from config/conf_conf/developers.json' do
|
21
|
+
subject.add(developer)
|
22
|
+
subject.remove(developer)
|
23
|
+
expect(subject.keys.length).to eq(0)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ConfConf::Project::Environment::Storage do
|
4
|
+
before do
|
5
|
+
stub_const('RbNaCl', MockRbNaCl)
|
6
|
+
stub_const('File', MockFile)
|
7
|
+
end
|
8
|
+
|
9
|
+
let(:developer) {
|
10
|
+
pretty_public_key = Base64.strict_encode64(developer_private_key.public_key.to_s)
|
11
|
+
ConfConf::Project::Developer.new(pretty_public_key)
|
12
|
+
}
|
13
|
+
|
14
|
+
let(:developer_private_key) {
|
15
|
+
RbNaCl::PrivateKey.generate
|
16
|
+
}
|
17
|
+
|
18
|
+
let(:project) {
|
19
|
+
project = ConfConf::Project.new
|
20
|
+
project.developers.add(developer)
|
21
|
+
project.developers.save
|
22
|
+
project
|
23
|
+
}
|
24
|
+
|
25
|
+
let(:environment) {
|
26
|
+
instance_double('ConfConf::Environment', {
|
27
|
+
name: environment_name,
|
28
|
+
schema: {},
|
29
|
+
variables: { 'VAR1' => 'VAL1' }
|
30
|
+
})
|
31
|
+
}
|
32
|
+
|
33
|
+
let(:environment_name) {
|
34
|
+
"environment-#{rand(10e6)}"
|
35
|
+
}
|
36
|
+
|
37
|
+
it 'encrypts and saves an environment' do
|
38
|
+
described_class.save_project_environment(project, environment)
|
39
|
+
expect(File.fs).to include("config/conf_conf/environments/#{environment_name}.json")
|
40
|
+
|
41
|
+
encrypted_environment_json = File.read("config/conf_conf/environments/#{environment_name}.json")
|
42
|
+
encrypted_environment_config = MultiJson.load(encrypted_environment_json)
|
43
|
+
encrypted_environment = RbNaCl::Info.new(encrypted_environment_config['encrypted_environment'])
|
44
|
+
|
45
|
+
expect(encrypted_environment.secret_key_encrypted?).to be(true)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'adds an encrypted version of the secret key for the author' do
|
49
|
+
described_class.save_project_environment(project, environment)
|
50
|
+
|
51
|
+
encrypted_environment_json = File.read("config/conf_conf/environments/#{environment_name}.json")
|
52
|
+
encrypted_environment_config = MultiJson.load(encrypted_environment_json)
|
53
|
+
|
54
|
+
author_public_key = encrypted_environment_config["author_public_key"]
|
55
|
+
developer_public_keys = encrypted_environment_config["encrypted_environment_secret_key"].keys
|
56
|
+
expect(developer_public_keys).to include(author_public_key)
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'loads an encrypted environment as an author' do
|
60
|
+
described_class.save_project_environment(project, environment)
|
61
|
+
environment = described_class.load_project_environment(project, environment_name)
|
62
|
+
|
63
|
+
expect(environment).to be_a(ConfConf::Project::Environment)
|
64
|
+
expect(environment.variables['VAR1']).to eq('VAL1')
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'loads an encrypted environment as an authorized developer' do
|
68
|
+
described_class.save_project_environment(project, environment)
|
69
|
+
|
70
|
+
pretty_developer_private_key = Base64.strict_encode64(developer_private_key.to_s)
|
71
|
+
current_developer = ConfConf::Project::Developer.new(developer.pretty_public_key, pretty_developer_private_key)
|
72
|
+
expect(ConfConf::Project::Developer).to receive(:current).and_return(current_developer)
|
73
|
+
|
74
|
+
environment = described_class.load_project_environment(project, environment_name)
|
75
|
+
expect(environment).to be_a(ConfConf::Project::Environment)
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'fails to load an encrypted environment when not authenticated' do
|
79
|
+
described_class.save_project_environment(project, environment)
|
80
|
+
|
81
|
+
private_key = RbNaCl::PrivateKey.generate
|
82
|
+
pretty_private_key = Base64.strict_encode64(private_key.to_s)
|
83
|
+
pretty_public_key = Base64.strict_encode64(private_key.public_key.to_s)
|
84
|
+
unauthorized_developer = ConfConf::Project::Developer.new(pretty_public_key, pretty_private_key)
|
85
|
+
expect(ConfConf::Project::Developer).to receive(:current).and_return(unauthorized_developer)
|
86
|
+
|
87
|
+
expect {
|
88
|
+
described_class.load_project_environment(project, environment_name)
|
89
|
+
}.to raise_error(ConfConf::Project::Environment::NotAuthorizedError)
|
90
|
+
end
|
91
|
+
end
|