veil 0.2.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.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/bin/console +6 -0
- data/bin/setup +8 -0
- data/bin/veil-env-helper +47 -0
- data/bin/veil-ingest-secret +41 -0
- data/lib/veil.rb +6 -0
- data/lib/veil/credential.rb +75 -0
- data/lib/veil/credential_collection.rb +18 -0
- data/lib/veil/credential_collection/base.rb +240 -0
- data/lib/veil/credential_collection/chef_secrets_file.rb +105 -0
- data/lib/veil/exceptions.rb +15 -0
- data/lib/veil/hasher.rb +29 -0
- data/lib/veil/hasher/base.rb +44 -0
- data/lib/veil/hasher/bcrypt.rb +59 -0
- data/lib/veil/hasher/pbkdf2.rb +54 -0
- data/lib/veil/utils.rb +25 -0
- data/lib/veil/version.rb +3 -0
- data/spec/credential_collection/base_spec.rb +433 -0
- data/spec/credential_collection/chef_secrets_file_spec.rb +142 -0
- data/spec/credential_collection_spec.rb +30 -0
- data/spec/credential_spec.rb +94 -0
- data/spec/hasher/base_spec.rb +19 -0
- data/spec/hasher/bcrypt_spec.rb +48 -0
- data/spec/hasher/pbkdf2_spec.rb +60 -0
- data/spec/hasher_spec.rb +34 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/utils_spec.rb +25 -0
- data/spec/veil_spec.rb +7 -0
- metadata +157 -0
@@ -0,0 +1,105 @@
|
|
1
|
+
require "veil/credential_collection/base"
|
2
|
+
require "fileutils"
|
3
|
+
require "json"
|
4
|
+
require "tempfile"
|
5
|
+
|
6
|
+
module Veil
|
7
|
+
class CredentialCollection
|
8
|
+
class ChefSecretsFile < Base
|
9
|
+
class << self
|
10
|
+
def from_file(path, opts = {})
|
11
|
+
unless File.exists?(path)
|
12
|
+
raise InvalidCredentialCollectionFile.new("#{path} does not exist")
|
13
|
+
end
|
14
|
+
|
15
|
+
new(opts.merge(path: path))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :path, :user, :group
|
20
|
+
|
21
|
+
# Create a new ChefSecretsFile
|
22
|
+
#
|
23
|
+
# @param [Hash] opts
|
24
|
+
# a hash of options to pass to the constructor
|
25
|
+
def initialize(opts = {})
|
26
|
+
@path = (opts[:path] && File.expand_path(opts[:path])) || "/etc/opscode/private-chef-secrets.json"
|
27
|
+
|
28
|
+
import_existing = File.exists?(path) && (File.size(path) != 0)
|
29
|
+
legacy = true
|
30
|
+
|
31
|
+
if import_existing
|
32
|
+
begin
|
33
|
+
hash = JSON.parse(IO.read(path), symbolize_names: true)
|
34
|
+
rescue JSON::ParserError, Errno::ENOENT => e
|
35
|
+
raise InvalidCredentialCollectionFile.new("#{path} is not a valid credentials file:\n #{e.message}")
|
36
|
+
end
|
37
|
+
|
38
|
+
if hash.key?(:veil) && hash[:veil][:type] == "Veil::CredentialCollection::ChefSecretsFile"
|
39
|
+
opts = Veil::Utils.symbolize_keys(hash[:veil]).merge(opts)
|
40
|
+
legacy = false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
@user = opts[:user]
|
45
|
+
@group = opts[:group] || @user
|
46
|
+
@version = opts[:version] || 1
|
47
|
+
super(opts)
|
48
|
+
|
49
|
+
import_legacy_credentials(hash) if import_existing && legacy
|
50
|
+
end
|
51
|
+
|
52
|
+
# Set the secrets file path
|
53
|
+
#
|
54
|
+
# @param [String] path
|
55
|
+
# a path to the private-chef-secrets.json
|
56
|
+
def path=(path)
|
57
|
+
@path = File.expand_path(path)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Save the CredentialCollection to file
|
61
|
+
def save
|
62
|
+
FileUtils.mkdir_p(File.dirname(path)) unless File.directory?(File.dirname(path))
|
63
|
+
|
64
|
+
f = Tempfile.new("veil") # defaults to mode 0600
|
65
|
+
FileUtils.chown(user, group, f.path) if user
|
66
|
+
f.puts(JSON.pretty_generate(secrets_hash))
|
67
|
+
f.flush
|
68
|
+
f.close
|
69
|
+
|
70
|
+
FileUtils.mv(f.path, path)
|
71
|
+
true
|
72
|
+
end
|
73
|
+
|
74
|
+
# Return the instance as a secrets style hash
|
75
|
+
def secrets_hash
|
76
|
+
{ "veil" => to_h }.merge(legacy_credentials_hash)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Return the credentials in a legacy chef secrets hash
|
80
|
+
def legacy_credentials_hash
|
81
|
+
hash = Hash.new
|
82
|
+
|
83
|
+
to_h[:credentials].each do |namespace, creds|
|
84
|
+
hash[namespace] = {}
|
85
|
+
creds.each { |name, cred| hash[namespace][name] = cred[:value] }
|
86
|
+
end
|
87
|
+
|
88
|
+
hash
|
89
|
+
end
|
90
|
+
|
91
|
+
def import_legacy_credentials(hash)
|
92
|
+
hash.each do |namespace, creds_hash|
|
93
|
+
credentials[namespace.to_s] ||= Hash.new
|
94
|
+
creds_hash.each do |cred, value|
|
95
|
+
credentials[namespace.to_s][cred.to_s] = Veil::Credential.new(
|
96
|
+
name: cred.to_s,
|
97
|
+
value: value,
|
98
|
+
length: value.length
|
99
|
+
)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Veil
|
2
|
+
class InvalidSalt < StandardError; end
|
3
|
+
class InvalidSecret < StandardError; end
|
4
|
+
class InvalidParameter < StandardError; end
|
5
|
+
class InvalidHasher < StandardError; end
|
6
|
+
class InvalidCredentialCollectionFile < StandardError; end
|
7
|
+
class MissingParameter < StandardError; end
|
8
|
+
class NotImplmented < StandardError; end
|
9
|
+
class InvalidCredentialHash < StandardError; end
|
10
|
+
class CredentialNotFound < StandardError; end
|
11
|
+
class GroupNotFound < StandardError; end
|
12
|
+
class FileNotFound < StandardError; end
|
13
|
+
class FileNotReadable < StandardError; end
|
14
|
+
class UnknownProvider < StandardError; end
|
15
|
+
end
|
data/lib/veil/hasher.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require "veil/utils"
|
2
|
+
require "veil/hasher/base"
|
3
|
+
require "veil/hasher/bcrypt"
|
4
|
+
require "veil/hasher/pbkdf2"
|
5
|
+
|
6
|
+
module Veil
|
7
|
+
class Hasher
|
8
|
+
DEFAULT_OPTIONS = {
|
9
|
+
type: "Veil::Hasher::PBKDF2",
|
10
|
+
iterations: 10_000,
|
11
|
+
hash_function: "SHA512"
|
12
|
+
}
|
13
|
+
|
14
|
+
class << self
|
15
|
+
#
|
16
|
+
# Create a new Hasher instance
|
17
|
+
#
|
18
|
+
# @param opts Hash<Symbol> a hash of options to pass to the constructor
|
19
|
+
#
|
20
|
+
# @example Veil::Hasher.create(type: "BCrypt", cost: 10)
|
21
|
+
# @example Veil::Hasher.create(type: "PBKDF2", iterations: 1000, hash_function: "SHA256")
|
22
|
+
#
|
23
|
+
def create(opts = {})
|
24
|
+
opts = Veil::Utils.symbolize_keys(DEFAULT_OPTIONS.merge(opts))
|
25
|
+
const_get(opts[:type]).new(opts)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require "openssl"
|
2
|
+
require "veil/exceptions"
|
3
|
+
|
4
|
+
module Veil
|
5
|
+
class Hasher
|
6
|
+
class Base
|
7
|
+
|
8
|
+
# Hash the credential group, name and version with the stored secret and salt
|
9
|
+
#
|
10
|
+
# @param [String] group
|
11
|
+
# The service group name, eg: postgresql
|
12
|
+
#
|
13
|
+
# @param [String] name
|
14
|
+
# The credential name, eg: sql_password
|
15
|
+
#
|
16
|
+
# @param [Integer] version
|
17
|
+
# The Credential version, eg: 1
|
18
|
+
#
|
19
|
+
# @return [String] SHA512 hex digest of hashed data
|
20
|
+
def encrypt(group, name, version)
|
21
|
+
raise Veil::NotImplmented.new("#{caller[0]} has not implemented #encrypt")
|
22
|
+
end
|
23
|
+
|
24
|
+
# Return the instance as a Hash
|
25
|
+
#
|
26
|
+
# @return [Hash<Symbol,String>]
|
27
|
+
def to_hash
|
28
|
+
raise Veil::NotImplmented.new("#{caller[0]} has not implemented #to_hash")
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# Create a SHA512 hex digest
|
34
|
+
#
|
35
|
+
# @param [String] data
|
36
|
+
# Data to digest
|
37
|
+
#
|
38
|
+
# @return [String]
|
39
|
+
def hex_digest(data)
|
40
|
+
OpenSSL::Digest::SHA512.hexdigest(data)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require "veil/hasher/base"
|
2
|
+
require "securerandom"
|
3
|
+
require "bcrypt"
|
4
|
+
|
5
|
+
module Veil
|
6
|
+
class Hasher
|
7
|
+
class BCrypt < Base
|
8
|
+
attr_reader :secret, :salt
|
9
|
+
|
10
|
+
# Create a new BCrypt
|
11
|
+
#
|
12
|
+
# @param [Hash] opts
|
13
|
+
# a hash of options to pass to the constructor
|
14
|
+
def initialize(opts = {})
|
15
|
+
if opts[:secret] && opts[:salt]
|
16
|
+
if ::BCrypt::Engine.valid_secret?(opts[:secret]) && ::BCrypt::Engine.valid_salt?(opts[:salt])
|
17
|
+
@secret = opts.delete(:secret)
|
18
|
+
@salt = opts.delete(:salt)
|
19
|
+
elsif ::BCrypt::Engine.valid_secret?(opts[:secret])
|
20
|
+
raise Veil::InvalidSalt.new("#{opts[:salt]} is not valid salt")
|
21
|
+
else
|
22
|
+
raise Veil::InvalidSecret.new("#{opts[:secret]} is not valid secret")
|
23
|
+
end
|
24
|
+
else
|
25
|
+
@secret = SecureRandom.hex(512)
|
26
|
+
@salt = ::BCrypt::Engine.generate_salt(opts[:cost] || 10)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Hash the credential group, name and version with the stored secret and salt
|
31
|
+
#
|
32
|
+
# @param [String] group
|
33
|
+
# The service group name, eg: postgresql
|
34
|
+
#
|
35
|
+
# @param [String] name
|
36
|
+
# The credential name, eg: sql_password
|
37
|
+
#
|
38
|
+
# @param [Integer] version
|
39
|
+
# The Credential version, eg: 1
|
40
|
+
#
|
41
|
+
# @return [String] SHA512 hex digest of hashed data
|
42
|
+
def encrypt(group, name, version)
|
43
|
+
hex_digest(::BCrypt::Engine.hash_secret(hex_digest([secret, group, name, version].join), salt))
|
44
|
+
end
|
45
|
+
|
46
|
+
# Return the instance as a Hash
|
47
|
+
#
|
48
|
+
# @return [Hash<Symbol,String>]
|
49
|
+
def to_hash
|
50
|
+
{
|
51
|
+
type: self.class.name,
|
52
|
+
secret: secret,
|
53
|
+
salt: salt,
|
54
|
+
}
|
55
|
+
end
|
56
|
+
alias_method :to_h, :to_hash
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require "veil/hasher/base"
|
2
|
+
require "securerandom"
|
3
|
+
|
4
|
+
module Veil
|
5
|
+
class Hasher
|
6
|
+
class PBKDF2 < Base
|
7
|
+
attr_reader :secret, :salt, :iterations, :hash_function
|
8
|
+
|
9
|
+
# Create a new PBKDF2
|
10
|
+
#
|
11
|
+
# @param [Hash] opts
|
12
|
+
# a hash of options to pass to the constructor
|
13
|
+
def initialize(opts = {})
|
14
|
+
@secret = opts[:secret] || SecureRandom.hex(512)
|
15
|
+
@salt = opts[:salt] || SecureRandom.hex(128)
|
16
|
+
@iterations = opts[:iterations] || 100_000
|
17
|
+
@hash_function = OpenSSL::Digest.const_get((opts[:hash_function] || "SHA512")).new
|
18
|
+
end
|
19
|
+
|
20
|
+
# Hash data with the stored secret and salt
|
21
|
+
#
|
22
|
+
# @param [String] data
|
23
|
+
# The service name and version to be encrypted with the shared key
|
24
|
+
#
|
25
|
+
# @param [Hash] opts
|
26
|
+
# Optional parameter overrides
|
27
|
+
#
|
28
|
+
# @return [String] SHA512 hex digest of hashed data
|
29
|
+
def encrypt(group, name, version)
|
30
|
+
hex_digest(OpenSSL::PKCS5.pbkdf2_hmac(
|
31
|
+
[secret, group, name, version].join,
|
32
|
+
salt,
|
33
|
+
iterations,
|
34
|
+
hash_function.length,
|
35
|
+
hash_function
|
36
|
+
))
|
37
|
+
end
|
38
|
+
|
39
|
+
# Return the instance as a Hash
|
40
|
+
#
|
41
|
+
# @return [Hash]
|
42
|
+
def to_hash
|
43
|
+
{
|
44
|
+
type: self.class.name,
|
45
|
+
secret: secret,
|
46
|
+
salt: salt,
|
47
|
+
iterations: iterations,
|
48
|
+
hash_function: hash_function.class.name
|
49
|
+
}
|
50
|
+
end
|
51
|
+
alias_method :to_h, :to_hash
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/veil/utils.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module Veil
|
2
|
+
module Utils
|
3
|
+
class << self
|
4
|
+
def symbolize_keys(hash)
|
5
|
+
new_hash = {}
|
6
|
+
hash.keys.each { |k| new_hash[k.to_sym] = hash.delete(k) }
|
7
|
+
new_hash
|
8
|
+
end
|
9
|
+
|
10
|
+
def symbolize_keys!(hash)
|
11
|
+
hash = symbolize_keys(hash)
|
12
|
+
end
|
13
|
+
|
14
|
+
def stringify_keys(hash)
|
15
|
+
new_hash = {}
|
16
|
+
hash.keys.each { |k| new_hash[k.to_s] = hash.delete(k) }
|
17
|
+
new_hash
|
18
|
+
end
|
19
|
+
|
20
|
+
def stringify_keys!(hash)
|
21
|
+
hash = stringify_keys(hash)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/veil/version.rb
ADDED
@@ -0,0 +1,433 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Veil::CredentialCollection::Base do
|
4
|
+
let(:salt) { "$2a$11$4xS0IHHxU5sOYNNZ5X53Qe" }
|
5
|
+
let(:secret) { "ultrasecure" }
|
6
|
+
let(:hasher) { Veil::Hasher.create(type: "BCrypt", secret: secret, salt: salt) }
|
7
|
+
|
8
|
+
subject { described_class.new(hasher: hasher.to_h) }
|
9
|
+
|
10
|
+
describe "#self.create" do
|
11
|
+
it "returns a credential store from the hash" do
|
12
|
+
expect(described_class.create.class).to eq(described_class)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "#new" do
|
17
|
+
context "with hasher options" do
|
18
|
+
it "builds the hasher instance" do
|
19
|
+
hasher_hash = subject.hasher.to_h
|
20
|
+
new_instance = described_class.new(hasher: hasher_hash)
|
21
|
+
expect(new_instance.hasher.to_h).to eq(hasher_hash)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context "with credential options" do
|
26
|
+
it "builds the credentials" do
|
27
|
+
subject.add("foo", "bar", length: 22)
|
28
|
+
creds_hash = subject.to_hash[:credentials]
|
29
|
+
|
30
|
+
new_instance = described_class.new(credentials: creds_hash)
|
31
|
+
expect(new_instance["foo"]["bar"].value).to eq(subject["foo"]["bar"].value)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "with version options" do
|
36
|
+
it "defaults to version 1" do
|
37
|
+
expect(subject.version).to eq(1)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "sets the version" do
|
41
|
+
new_instance = described_class.new(version: 12)
|
42
|
+
expect(new_instance.version).to eq(12)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "#get" do
|
48
|
+
before do
|
49
|
+
subject.add("testkey0", value: "testvalue0")
|
50
|
+
subject.add("testgroup", "testkey1", value: "testvalue1")
|
51
|
+
end
|
52
|
+
|
53
|
+
it "returns the value of a given credential" do
|
54
|
+
expect(subject.get("testkey0")).to eq("testvalue0")
|
55
|
+
end
|
56
|
+
|
57
|
+
it "returns the value of a given credential in a group" do
|
58
|
+
expect(subject.get("testgroup", "testkey1")).to eq("testvalue1")
|
59
|
+
end
|
60
|
+
|
61
|
+
it "raises an error if the credential isn't found" do
|
62
|
+
expect { subject.get("dne") }.to raise_error(Veil::CredentialNotFound)
|
63
|
+
end
|
64
|
+
|
65
|
+
it "raises an error if the group isn't found" do
|
66
|
+
expect { subject.get("dne", "tesetkey") }.to raise_error(Veil::GroupNotFound)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "raises an error if the credential isn't found in the group" do
|
70
|
+
expect { subject.get("testgroup", "dne") }.to raise_error(Veil::CredentialNotFound)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "raises an error if the wrong number of arguments are given" do
|
74
|
+
expect { subject.get("testgroup", "tesetkey", "whoops") }.to raise_error(ArgumentError)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "#exist?" do
|
79
|
+
it "returns false if the key does not exist" do
|
80
|
+
expect(subject.exist?("Invalid Key")).to eq(false)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "returns true if the key does exist" do
|
84
|
+
subject.add("testkey0", value: "testvalue0")
|
85
|
+
expect(subject.exist?("testkey0")).to eq(true)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "#add_from_file" do
|
90
|
+
context "when the file can be read" do
|
91
|
+
# using this as our input file lets us do less mocking of
|
92
|
+
# file sanity checks.
|
93
|
+
let (:input_file) { "/" }
|
94
|
+
let (:secret_content) { "a secret!" }
|
95
|
+
|
96
|
+
before do
|
97
|
+
allow(File).to receive(:read).with(input_file).and_return secret_content
|
98
|
+
end
|
99
|
+
|
100
|
+
context "with a name" do
|
101
|
+
it "adds the contents of the file as a credential" do
|
102
|
+
subject.add_from_file(input_file, "supersecret")
|
103
|
+
cred = subject["supersecret"]
|
104
|
+
expect(cred).to be_instance_of(Veil::Credential)
|
105
|
+
expect(cred.value).to eq secret_content
|
106
|
+
expect(cred.frozen).to be true
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
context "with a group and name" do
|
111
|
+
it "adds the contents of the file as a credential" do
|
112
|
+
subject.add_from_file(input_file, "super", "secret")
|
113
|
+
cred = subject["super"]["secret"]
|
114
|
+
expect(cred).to be_instance_of(Veil::Credential)
|
115
|
+
expect(cred.value).to eq secret_content
|
116
|
+
expect(cred.frozen).to be true
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context "when the file can not be read" do
|
122
|
+
let (:input_file) { "/invalid" }
|
123
|
+
|
124
|
+
it "fails with a FileNotReadable error" do
|
125
|
+
expect { subject.add_from_file(input_file, "supersecret") }.to raise_error(Veil::FileNotReadable)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe "#add" do
|
131
|
+
it "creates a new credential" do
|
132
|
+
subject.add("cowabunga")
|
133
|
+
expect(subject["cowabunga"]).to be_instance_of(Veil::Credential)
|
134
|
+
end
|
135
|
+
|
136
|
+
context "with a name" do
|
137
|
+
it "creates a new credential the right name" do
|
138
|
+
subject.add("cowabunga")
|
139
|
+
expect(subject["cowabunga"]).to be_instance_of(Veil::Credential)
|
140
|
+
expect(subject["cowabunga"].name).to eq("cowabunga")
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
context "with a group and name" do
|
145
|
+
it "creates a new credential the right group and name" do
|
146
|
+
subject.add("my_db", "password")
|
147
|
+
expect(subject["my_db"]["password"]).to be_instance_of(Veil::Credential)
|
148
|
+
expect(subject["my_db"]["password"].name).to eq("password")
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
context "with a name and length" do
|
153
|
+
it "creates a new credential the right name and length" do
|
154
|
+
subject.add("conspiracy", length: 23)
|
155
|
+
expect(subject["conspiracy"]).to be_instance_of(Veil::Credential)
|
156
|
+
expect(subject["conspiracy"].length).to eq(23)
|
157
|
+
expect(subject["conspiracy"].value.length).to eq(23)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
context "with a group, name, and length" do
|
162
|
+
it "creates a new credential the right group and name" do
|
163
|
+
subject.add("my_db", "password", length: 15)
|
164
|
+
expect(subject["my_db"]["password"]).to be_instance_of(Veil::Credential)
|
165
|
+
expect(subject["my_db"]["password"].name).to eq("password")
|
166
|
+
expect(subject["my_db"]["password"].length).to eq(15)
|
167
|
+
expect(subject["my_db"]["password"].value.length).to eq(15)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
context "with a group, name, default" do
|
172
|
+
it "creates a new credential the right group and name" do
|
173
|
+
subject.add("my_db", "password", value: "super_unison")
|
174
|
+
expect(subject["my_db"]["password"]).to be_instance_of(Veil::Credential)
|
175
|
+
expect(subject["my_db"]["password"].name).to eq("password")
|
176
|
+
expect(subject["my_db"]["password"].value).to eq("super_unison")
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
context "with a name and default" do
|
181
|
+
it "creates a new credential the right group and name" do
|
182
|
+
subject.add("luau", value: "new_math")
|
183
|
+
expect(subject["luau"]).to be_instance_of(Veil::Credential)
|
184
|
+
expect(subject["luau"].name).to eq("luau")
|
185
|
+
expect(subject["luau"].value).to eq("new_math")
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
context "when the credential already exists" do
|
190
|
+
it "does not overwrite it" do
|
191
|
+
subject.add("my_db", "password", length: 15)
|
192
|
+
val = subject["my_db"]["password"].value
|
193
|
+
subject.add("my_db", "password", value: "new-password")
|
194
|
+
expect(subject["my_db"]["password"].value).to eq(val)
|
195
|
+
end
|
196
|
+
|
197
|
+
it "returns the existing credential" do
|
198
|
+
subject.add("my_db", "password", length: 15)
|
199
|
+
my_db = subject["my_db"]["password"]
|
200
|
+
expect(subject.add("my_db", "password")).to eq(my_db)
|
201
|
+
end
|
202
|
+
|
203
|
+
context "when force: true is given as param" do
|
204
|
+
it "does overwrite it" do
|
205
|
+
subject.add("my_db", "password", length: 15)
|
206
|
+
subject.add("my_db", "password", value: 'new-password', force: true)
|
207
|
+
expect(subject["my_db"]["password"].value).to eq("new-password")
|
208
|
+
end
|
209
|
+
|
210
|
+
it "returns the new credential" do
|
211
|
+
subject.add("my_db", "password", length: 15)
|
212
|
+
expect(subject.add("my_db", "password", value: "new-password", force: true).value).to eq('new-password')
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
context "when force: true is given as param and :frozen is not" do
|
217
|
+
it "sets frozen to true" do
|
218
|
+
subject.add("my_db", "password", value: 'new-password', force: true)
|
219
|
+
expect(subject["my_db"]["password"].frozen).to eq(true)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
context "when force: true is given as param and :frozen is false" do
|
224
|
+
it "sets frozen to false" do
|
225
|
+
subject.add("my_db", "password", value: 'new-password', force: true, frozen: false)
|
226
|
+
expect(subject["my_db"]["password"].frozen).to eq(false)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
context "when force: false is given as param and :frozen is true" do
|
231
|
+
it "sets frozen to true" do
|
232
|
+
subject.add("my_db", "password", value: 'new-password', force: true, frozen: true)
|
233
|
+
expect(subject["my_db"]["password"].frozen).to eq(true)
|
234
|
+
end
|
235
|
+
|
236
|
+
end
|
237
|
+
|
238
|
+
context "when force: false is given as param and :frozen is false" do
|
239
|
+
it "sets frozen to false" do
|
240
|
+
subject.add("my_db", "password", value: 'new-password', force: true, frozen: false)
|
241
|
+
expect(subject["my_db"]["password"].frozen).to eq(false)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
describe "#remove" do
|
248
|
+
context "with a cred" do
|
249
|
+
context "with a match" do
|
250
|
+
it "returns the value and removes the credential" do
|
251
|
+
subject.add("funk")
|
252
|
+
value = subject["funk"]
|
253
|
+
expect(subject.remove("funk")).to eq(value)
|
254
|
+
expect(subject["funk"]).to be_nil
|
255
|
+
end
|
256
|
+
|
257
|
+
context "when there is not a match" do
|
258
|
+
it "returns nil" do
|
259
|
+
expect(subject.remove("not_a_cred")).to be_nil
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
context "with a group and cred" do
|
266
|
+
context "with a match" do
|
267
|
+
it "returns the value and removes the credential" do
|
268
|
+
subject.add("grandfunk", "railroad")
|
269
|
+
value = subject["grandfunk"]["railroad"]
|
270
|
+
expect(subject.remove("grandfunk", "railroad")).to eq(value)
|
271
|
+
expect(subject["grandfunk"]["railroad"]).to be_nil
|
272
|
+
end
|
273
|
+
|
274
|
+
context "when there is not a match" do
|
275
|
+
it "returns nil" do
|
276
|
+
expect(subject.remove("nested", "thing")).to be_nil
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
describe "#rotate_hasher" do
|
284
|
+
it "creates a new hasher" do
|
285
|
+
hasher = subject.hasher
|
286
|
+
subject.rotate_hasher
|
287
|
+
expect(subject.hasher).to_not eq(hasher)
|
288
|
+
end
|
289
|
+
|
290
|
+
it "rotates all credentials" do
|
291
|
+
subject.add("foo")
|
292
|
+
foo_val = subject["foo"].value
|
293
|
+
subject.add("bar", "baz", length: 25)
|
294
|
+
baz_val = subject["bar"]["baz"].value
|
295
|
+
|
296
|
+
subject.rotate_hasher
|
297
|
+
|
298
|
+
expect(subject["foo"].value).to_not eq(foo_val)
|
299
|
+
expect(subject["foo"].version).to eq(1)
|
300
|
+
expect(subject["bar"]["baz"].value).to_not eq(baz_val)
|
301
|
+
expect(subject["bar"]["baz"].version).to eq(1)
|
302
|
+
end
|
303
|
+
|
304
|
+
context "with frozen credentials" do
|
305
|
+
it "rotates all credentials that are not frozen" do
|
306
|
+
subject.add("foo")
|
307
|
+
foo_val = subject["foo"].value
|
308
|
+
subject.add("bar", "baz", length: 25)
|
309
|
+
baz_val = subject["bar"]["baz"].value
|
310
|
+
subject.add("qux", "quux", frozen: true)
|
311
|
+
baz_val = subject["qux"]["quux"].value
|
312
|
+
|
313
|
+
subject.rotate_hasher
|
314
|
+
|
315
|
+
expect(subject["foo"].value).to_not eq(foo_val)
|
316
|
+
expect(subject["foo"].version).to eq(1)
|
317
|
+
expect(subject["bar"]["baz"].value).to_not eq(baz_val)
|
318
|
+
expect(subject["bar"]["baz"].version).to eq(1)
|
319
|
+
expect(subject["qux"]["quux"].value).to eq(baz_val)
|
320
|
+
expect(subject["qux"]["quux"].version).to eq(0)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
describe "#rotate_credentials" do
|
326
|
+
it "doesn't create a new hasher" do
|
327
|
+
hasher = subject.hasher
|
328
|
+
subject.rotate_credentials
|
329
|
+
expect(subject.hasher).to eq(hasher)
|
330
|
+
end
|
331
|
+
|
332
|
+
it "rotates all credentials" do
|
333
|
+
subject.add("foo")
|
334
|
+
foo_val = subject["foo"].value
|
335
|
+
subject.add("bar", "baz", length: 25)
|
336
|
+
baz_val = subject["bar"]["baz"].value
|
337
|
+
|
338
|
+
subject.rotate_credentials
|
339
|
+
|
340
|
+
expect(subject["foo"].value).to_not eq(foo_val)
|
341
|
+
expect(subject["foo"].version).to eq(1)
|
342
|
+
expect(subject["bar"]["baz"].value).to_not eq(baz_val)
|
343
|
+
expect(subject["bar"]["baz"].version).to eq(1)
|
344
|
+
end
|
345
|
+
|
346
|
+
context "with frozen credentials" do
|
347
|
+
it "rotates all credentials that are not frozen" do
|
348
|
+
subject.add("foo")
|
349
|
+
foo_val = subject["foo"].value
|
350
|
+
subject.add("bar", "baz", length: 25)
|
351
|
+
baz_val = subject["bar"]["baz"].value
|
352
|
+
subject.add("qux", "quux", frozen: true)
|
353
|
+
baz_val = subject["qux"]["quux"].value
|
354
|
+
|
355
|
+
subject.rotate_credentials
|
356
|
+
|
357
|
+
expect(subject["foo"].value).to_not eq(foo_val)
|
358
|
+
expect(subject["foo"].version).to eq(1)
|
359
|
+
expect(subject["bar"]["baz"].value).to_not eq(baz_val)
|
360
|
+
expect(subject["bar"]["baz"].version).to eq(1)
|
361
|
+
expect(subject["qux"]["quux"].value).to eq(baz_val)
|
362
|
+
expect(subject["qux"]["quux"].version).to eq(0)
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
describe "#rotate" do
|
368
|
+
context "when the credential exists" do
|
369
|
+
it "rotates the credential" do
|
370
|
+
subject.add("life_choices")
|
371
|
+
old_val = subject["life_choices"].value
|
372
|
+
old_version = subject["life_choices"].version
|
373
|
+
|
374
|
+
subject.rotate("life_choices")
|
375
|
+
expect(subject["life_choices"].value).to_not eq(old_val)
|
376
|
+
expect(subject["life_choices"].version).to eq(old_version + 1)
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
context "when the credential does not exist" do
|
381
|
+
it "returns nil" do
|
382
|
+
expect(subject.rotate("not_a_cred")).to be_nil
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
context "when passed a set name only" do
|
387
|
+
it "rotates each credential" do
|
388
|
+
subject.add("desert", "black_eagle")
|
389
|
+
eagle_val = subject["desert"]["black_eagle"].value
|
390
|
+
|
391
|
+
subject.add("desert", "mercury_six")
|
392
|
+
mercury_val = subject["desert"]["mercury_six"].value
|
393
|
+
|
394
|
+
subject.rotate("desert")
|
395
|
+
|
396
|
+
expect(subject["desert"]["black_eagle"].value).to_not eq(eagle_val)
|
397
|
+
expect(subject["desert"]["black_eagle"].version).to eq(1)
|
398
|
+
expect(subject["desert"]["mercury_six"].value).to_not eq(mercury_val)
|
399
|
+
expect(subject["desert"]["mercury_six"].version).to eq(1)
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
context "with a frozen credential" do
|
404
|
+
it "does not rotate the credential" do
|
405
|
+
subject.add("mannequin", "republic", frozen: true)
|
406
|
+
old_val = subject["mannequin"]["republic"].value
|
407
|
+
|
408
|
+
subject.rotate("mannequin", "republic")
|
409
|
+
|
410
|
+
expect(subject["mannequin"]["republic"].value).to eq(old_val)
|
411
|
+
expect(subject["mannequin"]["republic"].version).to eq(0)
|
412
|
+
end
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
describe "#to_hash" do
|
417
|
+
it "returns a valid hash" do
|
418
|
+
subject.add("foo")
|
419
|
+
subject.add("bar", "baz", length: 31)
|
420
|
+
subject.add("saint", "matthew", frozen: true)
|
421
|
+
|
422
|
+
new_instance = described_class.new(subject.to_hash)
|
423
|
+
expect(new_instance["foo"].version).to eq(subject["foo"].version)
|
424
|
+
expect(new_instance["foo"].value).to eq(subject["foo"].value)
|
425
|
+
expect(new_instance["bar"]["baz"].version).to eq(subject["bar"]["baz"].version)
|
426
|
+
expect(new_instance["bar"]["baz"].value).to eq(subject["bar"]["baz"].value)
|
427
|
+
expect(new_instance["bar"]["baz"].length).to eq(subject["bar"]["baz"].length)
|
428
|
+
expect(new_instance["saint"]["matthew"].version).to eq(subject["saint"]["matthew"].version)
|
429
|
+
expect(new_instance["saint"]["matthew"].value).to eq(subject["saint"]["matthew"].value)
|
430
|
+
expect(new_instance["saint"]["matthew"].frozen).to eq(subject["saint"]["matthew"].frozen)
|
431
|
+
end
|
432
|
+
end
|
433
|
+
end
|