diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 166649fc24a00ca2087af1d4d7cd14b7cd193897..6ba3731899d29f5b86ea937cdb60d2b6abf339c2 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -6,6 +6,7 @@ Settings['ldap'] ||= Settingslogic.new({}) Settings.ldap['enabled'] = false if Settings.ldap['enabled'].nil? Settings.ldap['prevent_ldap_sign_in'] = false if Settings.ldap['prevent_ldap_sign_in'].blank? +Settings.ldap['secret_file'] = Settings.absolute(Settings.ldap['secret_file'] || Rails.root.join("config/ldap_secret.yaml.enc")) Gitlab.ee do Settings.ldap['sync_time'] = 3600 if Settings.ldap['sync_time'].nil? diff --git a/config/settings.rb b/config/settings.rb index 99f1b85202e39d0a1ae8d3277fbb4e29d3401fae..4c070317b46bc5a8e81c01ff8f7f4119a78ee9c5 100644 --- a/config/settings.rb +++ b/config/settings.rb @@ -152,6 +152,15 @@ def attr_encrypted_db_key_base Gitlab::Application.secrets.db_key_base end + def encrypted(path, allow_in_safe_mode: false) + return Gitlab::EncryptedConfiguration.new if ENV['GITLAB_ENCRYPTED_SAFE_MODE'] && !allow_in_safe_mode + + Gitlab::EncryptedConfiguration.new( + config_path: Settings.absolute(path), + key: Settings.attr_encrypted_db_key_base_truncated + ) + end + def load_dynamic_cron_schedules! cron_jobs['gitlab_usage_ping_worker']['cron'] ||= cron_for_usage_ping end diff --git a/lib/gitlab/auth/ldap/config.rb b/lib/gitlab/auth/ldap/config.rb index 7677189eb9f5e32777ad640ff574f72a2533c4eb..d51774ff321132bf9627f86bb81774824fa24ba5 100644 --- a/lib/gitlab/auth/ldap/config.rb +++ b/lib/gitlab/auth/ldap/config.rb @@ -53,6 +53,10 @@ def self.invalid_provider(provider) raise InvalidProvider.new("Unknown provider (#{provider}). Available providers: #{providers}") end + def self.encrypted_secrets(allow_in_safe_mode: false) + Settings.encrypted(Gitlab.config.ldap.secret_file, allow_in_safe_mode: allow_in_safe_mode) + end + def initialize(provider) if self.class.valid_provider?(provider) @provider = provider @@ -90,7 +94,7 @@ def omniauth_options if has_auth? opts.merge!( bind_dn: options['bind_dn'], - password: options['password'] + password: auth_password ) end @@ -155,7 +159,7 @@ def external_groups end def has_auth? - options['password'] || options['bind_dn'] + auth_password || options['bind_dn'] end def allow_username_or_email_login @@ -268,11 +272,21 @@ def auth_options auth: { method: :simple, username: options['bind_dn'], - password: options['password'] + password: auth_password } } end + def secrets + self.class.encrypted_secrets[@provider.delete_prefix('ldap').to_sym] + end + + def auth_password + return options['password'] if options['password'] + + secrets&.fetch(:password)&.chomp + end + def omniauth_user_filter uid_filter = Net::LDAP::Filter.eq(uid, '%{username}') diff --git a/lib/gitlab/encrypted_configuration.rb b/lib/gitlab/encrypted_configuration.rb new file mode 100644 index 0000000000000000000000000000000000000000..8ff4491aeac69c156cccf63f221a667fc027211a --- /dev/null +++ b/lib/gitlab/encrypted_configuration.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Gitlab + class EncryptedConfiguration < ActiveSupport::EncryptedConfiguration + attr_reader :key + + def initialize(config_path: nil, key: nil) + @content_path = Pathname.new(config_path).yield_self { |path| path.symlink? ? path.realpath : path } if config_path + @key = key + end + + def read_env_key + nil + end + + def read_key_file + nil + end + end +end diff --git a/lib/gitlab/encrypted_ldap_command.rb b/lib/gitlab/encrypted_ldap_command.rb new file mode 100644 index 0000000000000000000000000000000000000000..44d7140d4e87219245097ad32f1e58a3af2e7dbe --- /dev/null +++ b/lib/gitlab/encrypted_ldap_command.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "rails/command/helpers/editor" + +# rubocop:disable Rails/Output +module Gitlab + class EncryptedLdapCommand + class << self + include Rails::Command::Helpers::Editor + + alias_method :say, :puts + + def edit + encrypted = Gitlab::Auth::Ldap::Config.encrypted_secrets(allow_in_safe_mode: true) + + editor = ENV['EDITOR'] || 'editor' + + catch_editing_exceptions do + encrypted.write(encrypted_file_template) unless File.exist?(encrypted.content_path) + encrypted.change do |tmp_path| + system("#{editor} #{tmp_path}") + end + end + + puts "File encrypted and saved." + rescue ActiveSupport::MessageEncryptor::InvalidMessage + puts "Couldn't decrypt #{encrypted.content_path}. Perhaps you passed the wrong key?" + end + + def show + encrypted = Gitlab::Auth::Ldap::Config.encrypted_secrets(allow_in_safe_mode: true) + + puts encrypted.read.presence || "File '#{encrypted.content_path}' does not exist. Use `rake gitlab:ldap:secret:edit` to change that." + end + + private + + def encrypted_file_template + <<~YAML + # main: + # password: '123' + YAML + end + end + end +end +# rubocop:enable Rails/Output diff --git a/lib/tasks/gitlab/encrypted.rake b/lib/tasks/gitlab/encrypted.rake new file mode 100644 index 0000000000000000000000000000000000000000..35b7ba56c410207f78d43e7fc87accd7e56bf68d --- /dev/null +++ b/lib/tasks/gitlab/encrypted.rake @@ -0,0 +1,7 @@ +namespace :gitlab do + namespace :encrypted do + task :safe_mode do + ENV['GITLAB_ENCRYPTED_SAFE_MODE'] = 'true' + end + end +end diff --git a/lib/tasks/gitlab/ldap.rake b/lib/tasks/gitlab/ldap.rake index 0459de27c965caa1b5318652d63afeb38915671c..b686362a4e3218bda36242d91ef75ca3ce9b2c69 100644 --- a/lib/tasks/gitlab/ldap.rake +++ b/lib/tasks/gitlab/ldap.rake @@ -36,5 +36,17 @@ namespace :gitlab do puts "Successfully updated #{plural_updated_count} out of #{plural_id_count} total" end end + + namespace :secret do + desc 'GitLab | LDAP | Secret | Edit ldap secrets' + task edit: ['gitlab:encrypted:safe_mode', :environment] do + Gitlab::EncryptedLdapCommand.edit + end + + desc 'GitLab | LDAP | Secret | Show ldap secrets' + task show: ['gitlab:encrypted:safe_mode', :environment] do + Gitlab::EncryptedLdapCommand.show + end + end end end diff --git a/spec/config/settings_spec.rb b/spec/config/settings_spec.rb index 9db3d35cbe5672222531e733435572186f79f88a..1f1f7957fe75dfdb2521b173e7feee5763cbdb7f 100644 --- a/spec/config/settings_spec.rb +++ b/spec/config/settings_spec.rb @@ -112,4 +112,21 @@ end end end + + describe '.encrypted' do + it 'defaults to using the db_key_base for the key' do + expect(Gitlab::EncryptedConfiguration).to receive(:new).with(hash_including(key: Settings.attr_encrypted_db_key_base_truncated)) + Settings.encrypted('tmp/tests/test.enc') + end + + it 'defaults the configpath within the rails root' do + expect(Settings.encrypted('tmp/tests/test.enc').content_path.fnmatch?(File.join(Rails.root, '**'))).to be true + end + + it 'returns empty encrypted config when in safe mode' do + stub_env('GITLAB_ENCRYPTED_SAFE_MODE', 'true') + expect(Gitlab::EncryptedConfiguration).to receive(:new).with(no_args) + Settings.encrypted('tmp/tests/test.enc') + end + end end diff --git a/spec/lib/gitlab/encrypted_configuration_spec.rb b/spec/lib/gitlab/encrypted_configuration_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a14d696486f02809801f43ccbc99ebccfbcdfaae --- /dev/null +++ b/spec/lib/gitlab/encrypted_configuration_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Gitlab::EncryptedConfiguration do + subject(:configuration) { described_class.new } + + describe '#initialize' do + it 'accepts all args as optional fields' do + expect { configuration }.not_to raise_exception + + expect(configuration.key).to be_nil + expect(configuration.read_env_key).to be_nil + expect(configuration.read_key_file).to be_nil + end + end + + context 'when provided key and config file' do + let!(:config_tmp_dir) { Dir.mktmpdir('config-') } + let(:credentials_config_path) { File.join(config_tmp_dir, 'credentials.yml.enc') } + let(:credentials_key) { ActiveSupport::EncryptedConfiguration.generate_key } + + after do + FileUtils.rm_f(config_tmp_dir) + end + + describe '#write' do + it 'encrypts the file using the provided key' do + encryptor = ActiveSupport::MessageEncryptor.new([credentials_key].pack('H*'), cipher: 'aes-128-gcm') + config = described_class.new(config_path: credentials_config_path, key: credentials_key) + + config.write('sample-content') + expect(encryptor.decrypt_and_verify(File.read(credentials_config_path))).to eq('sample-content') + end + end + + describe '#read' do + it 'reads yaml configuration' do + config = described_class.new(config_path: credentials_config_path, key: credentials_key) + + config.write({ foo: { bar: true } }.to_yaml) + expect(config.foo[:bar]).to be true + end + end + + describe '#change' do + it 'changes yaml configuration' do + config = described_class.new(config_path: credentials_config_path, key: credentials_key) + + config.write({ foo: { bar: true } }.to_yaml) + config.change do |unencrypted_file| + contents = YAML.safe_load(unencrypted_file.read, [Symbol]) + unencrypted_file.write contents.merge(beef: "stew").to_yaml + end + expect(config.foo[:bar]).to be true + expect(config.beef).to eq('stew') + end + end + end +end diff --git a/spec/tasks/gitlab/ldap_rake_spec.rb b/spec/tasks/gitlab/ldap_rake_spec.rb index bbc3f6250886619b81e2116390d5134124d603c4..23ee54658389a575e0e064b1a594bfd9bcf932dd 100644 --- a/spec/tasks/gitlab/ldap_rake_spec.rb +++ b/spec/tasks/gitlab/ldap_rake_spec.rb @@ -13,3 +13,41 @@ run_rake_task('gitlab:ldap:rename_provider', 'ldapmain', 'ldapfoo') end end + +describe 'gitlab:ldap:secret rake tasks' do + let(:ldap_secret_file) { 'tmp/tests/ldapenc/ldap_secret.yaml.enc' } + + before do + Rake.application.rake_require 'tasks/gitlab/encrypted' + Rake.application.rake_require 'tasks/gitlab/ldap' + stub_env('EDITOR', 'cat') + stub_warn_user_is_not_gitlab + FileUtils.mkdir_p('tmp/tests/ldapenc/') + allow(Gitlab.config.ldap).to receive(:secret_file).and_return(ldap_secret_file) + end + + after do + FileUtils.rm_rf('tmp/tests/ldapenc/') + end + + describe ':show' do + it 'displays error when file does not exist' do + expect { run_rake_task('gitlab:ldap:secret:show') }.to output(/File .* does not exist. Use `rake gitlab:ldap:secret:edit` to change that./).to_stdout + end + + it 'outputs the unencrypted content when present' do + encrypted = Settings.encrypted(ldap_secret_file, allow_in_safe_mode: true) + encrypted.write('somevalue') + expect { run_rake_task('gitlab:ldap:secret:show') }.to output(/somevalue/).to_stdout + end + end + + describe 'edit' do + it 'creates encrypted file' do + expect { run_rake_task('gitlab:ldap:secret:edit') }.to output(/File encrypted and saved./).to_stdout + expect(File.exist?(ldap_secret_file)).to be true + value = Settings.encrypted(ldap_secret_file, allow_in_safe_mode: true) + expect(value.read).to match(/password: '123'/) + end + end +end