1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
|
# frozen_string_literal: true
require 'spec_helper'
require_relative '../../config/initializers/01_secret_token'
# rubocop:disable RSpec/SpecFilePathFormat -- The initializer name starts with `01` because we want to run it ASAP
# rubocop:disable RSpec/FeatureCategory -- This is a shared responsibility
RSpec.describe SecretsInitializer do
let(:rails_env_name) { 'test' }
let(:rails_env) { ActiveSupport::EnvironmentInquirer.new(rails_env_name) }
let(:fake_secret_file) { Tempfile.new(['fake-secrets', '.yml']) }
let(:secrets_hash) { {} }
let(:fake_secret_file_content) { secrets_hash.to_yaml }
before do
fake_secret_file.write(fake_secret_file_content)
fake_secret_file.rewind
end
after do
fake_secret_file.close
fake_secret_file.unlink
end
subject(:initializer) { described_class.new(secrets_file_path: fake_secret_file.path, rails_env: rails_env) }
describe 'ensure acknowledged secrets in any installations' do
let(:acknowledged_secrets) do
%w[secret_key_base otp_key_base db_key_base openid_connect_signing_key encrypted_settings_key_base
rotated_encrypted_settings_key_base]
end
it 'does not allow to add a new secret without a proper handling' do
secrets_hash = YAML.safe_load_file(Rails.root.join('config/secrets.yml'))
secrets_hash.each_value do |secrets|
new_secrets = secrets.keys - acknowledged_secrets
expect(new_secrets).to be_empty,
<<~WARNING
CAUTION:
It looks like you have just added new secret(s) #{new_secrets.inspect} to the secrets.yml.
Please read the development guide for GitLab secrets at doc/development/application_secrets.md before you proceed this change.
If you're absolutely sure that the change is safe, please add the new secrets to the 'acknowledged_secrets' in order to silence this warning.
WARNING
end
end
end
describe '#secrets_from_file' do
context 'when the secrets files is a valid YAML' do
let(:secrets_hash) { { 'foo' => 'bar' } }
it 'parses and returns the hash' do
expect(initializer.secrets_from_file).to eq({ 'foo' => 'bar' })
end
end
context 'when the secrets file does not exist' do
let(:fake_secret_file_content) { 'foo = bar' }
it 'returns an empty hash' do
expect(YAML).to receive(:safe_load_file).and_raise(Errno::ENOENT)
expect(initializer.secrets_from_file).to eq({})
end
end
context 'when the secrets file contains invalid YAML' do
let(:fake_secret_file_content) { "foo:\n\tbar: baz\n\tbar: foo" }
it 'raises a Psych::SyntaxError exception' do
expect { initializer.secrets_from_file }.to raise_error(Psych::SyntaxError)
end
end
end
describe '#execute!' do
include StubENV
let(:allowed_keys) do
%w[
secret_key_base
db_key_base
otp_key_base
openid_connect_signing_key
]
end
let(:hex_key) { /\h{128}/ }
let(:rsa_key) { /\A-----BEGIN RSA PRIVATE KEY-----\n.+\n-----END RSA PRIVATE KEY-----\n\Z/m }
around do |example|
# We store Rails.application.credentials as a hash so that we can revert to the original
# values after the example has run. Assigning Rails.application.credentials= directly doesn't work.
original_credentials = Rails.application.credentials.to_h
# Ensure we clear any existing `encrypted_settings_key_base` credential
allowed_keys.each do |key|
Rails.application.credentials.public_send(:"#{key}=", nil)
end
example.run
original_credentials.each do |key, value|
Rails.application.credentials.public_send(:"#{key}=", value)
end
end
before do
allow(File).to receive(:write).with(fake_secret_file.path, any_args)
end
context 'when none of the secrets exist' do
before do
stub_env('SECRET_KEY_BASE', nil)
end
it 'generates different hashes for secret_key_base, otp_key_base, and db_key_base' do
initializer.execute!
keys = Rails.application.credentials.values_at(:secret_key_base, :otp_key_base, :db_key_base)
expect(keys.uniq).to eq(keys)
expect(keys).to all(match(hex_key))
end
it 'generates an RSA key for openid_connect_signing_key' do
initializer.execute!
keys = Rails.application.credentials.values_at(:openid_connect_signing_key)
expect(keys.uniq).to eq(keys)
expect(keys).to all(match(rsa_key))
end
it 'warns about the secrets to add to secrets.yml' do
allowed_keys.each do |key|
expect(initializer).to receive(:warn_missing_secret).with(key)
end
initializer.execute!
end
it 'writes the secrets to secrets.yml' do
expect(File).to receive(:write).with(fake_secret_file.path, any_args) do |_filename, contents, _options|
new_secrets = YAML.safe_load(contents)[rails_env_name]
allowed_keys.each do |key|
expect(new_secrets[key]).to eq(Rails.application.credentials.values_at(key.to_sym).first)
end
expect(new_secrets['encrypted_settings_key_base']).to be_nil # encrypted_settings_key_base is optional
end
initializer.execute!
end
context 'when GITLAB_GENERATE_ENCRYPTED_SETTINGS_KEY_BASE is set' do
let(:allowed_keys) do
super() + ['encrypted_settings_key_base']
end
before do
stub_env('GITLAB_GENERATE_ENCRYPTED_SETTINGS_KEY_BASE', '1')
allow(initializer).to receive(:warn_missing_secret)
end
it 'writes the encrypted_settings_key_base secret' do
expect(initializer).to receive(:warn_missing_secret).with('encrypted_settings_key_base')
expect(File).to receive(:write).with(fake_secret_file.path, any_args) do |_filename, contents, _options|
new_secrets = YAML.safe_load(contents)[rails_env_name]
expect(new_secrets['encrypted_settings_key_base']).to eq(Rails.application.credentials.encrypted_settings_key_base)
end
initializer.execute!
end
end
end
shared_examples 'credentials are properly set' do
it 'sets Rails.application.credentials' do
initializer.execute!
expect(Rails.application.credentials.values_at(*allowed_keys.map(&:to_sym))).to eq(allowed_keys)
end
it 'does not issue warnings' do
expect(initializer).not_to receive(:warn_missing_secret)
initializer.execute!
end
it 'does not update secrets.yml' do
expect(File).not_to receive(:write)
initializer.execute!
end
end
context 'when secrets exist in secrets.yml' do
let(:secrets_hash) { { rails_env_name => Hash[allowed_keys.zip(allowed_keys)] } }
it_behaves_like 'credentials are properly set'
context 'when secret_key_base also exist in the environment variable' do
before do
stub_env('SECRET_KEY_BASE', 'env_key')
end
it 'sets Rails.application.credentials.secret_key_base from the environment variable' do
initializer.execute!
expect(Rails.application.credentials.secret_key_base).to eq('env_key')
end
end
end
context 'when secrets exist in Rails.application.credentials' do
before do
allowed_keys.each do |key|
Rails.application.credentials.public_send(:"#{key}=", key)
end
end
it_behaves_like 'credentials are properly set'
context 'when secret_key_base also exist in the environment variable' do
before do
stub_env('SECRET_KEY_BASE', 'env_key')
end
it 'sets Rails.application.credentials.secret_key_base from the environment variable' do
initializer.execute!
expect(Rails.application.credentials.secret_key_base).to eq('env_key')
end
end
end
context 'with some secrets missing, some in ENV, some in Rails.application.credentials, some in secrets.yml' do
let(:rails_env_name) { 'foo' }
let(:secrets_hash) { { rails_env_name => { 'otp_key_base' => 'otp_key_base' } } }
before do
stub_env('SECRET_KEY_BASE', 'env_key')
Rails.application.credentials.db_key_base = 'db_key_base'
end
it 'sets Rails.application.credentials properly, issue a warning and writes config.secrets.yml' do
expect(File).to receive(:write).with(fake_secret_file.path, any_args) do |_filename, contents, _options|
new_secrets = YAML.safe_load(contents)[rails_env_name]
expect(new_secrets['otp_key_base']).to eq('otp_key_base')
expect(new_secrets['openid_connect_signing_key']).to match(rsa_key)
end
expect(initializer).to receive(:warn).with(/^Creating a backup of secrets file/)
expect(initializer).to receive(:warn).with(
"Missing Rails.application.credentials.openid_connect_signing_key for #{rails_env_name} environment. " \
"The secret will be generated and stored in config/secrets.yml."
)
expect(FileUtils).to receive(:mv).with(fake_secret_file.path, anything)
initializer.execute!
expect(Rails.application.credentials.secret_key_base).to eq('env_key')
expect(Rails.application.credentials.db_key_base).to eq('db_key_base')
expect(Rails.application.credentials.otp_key_base).to eq('otp_key_base')
end
end
end
end
# rubocop:enable RSpec/FeatureCategory
# rubocop:enable RSpec/SpecFilePathFormat
|