conf_conf 1.0.2 → 2.0.2

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