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.
@@ -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