Commit 37b80b40 authored by Gosia Ksionek's avatar Gosia Ksionek Committed by Mayra Cabrera

Create migration for token-iv pair

And new model

Add index to new table

Add saving token with iv

Add changelog entry

Add new column to new table

WIP

Change method to look for iv

Fix rubocop offences

Fix some rubocop offences

Fix more specs

Fix migration file and spec

Fix specs in migrations

WiP on specs

Add specs for model

WIP

Add spec for new method behauviour

Fix rubocop offences

Add cr remarks

Fix migration file

Add cr remark

Simplify one method

Add option for read only db

Add cr remarks

Add cr remarks

Add cr remarks
parent 78e313c0
......@@ -85,10 +85,18 @@ module TokenAuthenticatableStrategies
end
def find_by_encrypted_token(token, unscoped)
encrypted_value = Gitlab::CryptoHelper.aes256_gcm_encrypt(token)
nonce = Feature.enabled?(:dynamic_nonce_creation) ? find_hashed_iv(token) : Gitlab::CryptoHelper::AES256_GCM_IV_STATIC
encrypted_value = Gitlab::CryptoHelper.aes256_gcm_encrypt(token, nonce: nonce)
relation(unscoped).find_by(encrypted_field => encrypted_value)
end
def find_hashed_iv(token)
token_record = TokenWithIv.find_by_plaintext_token(token)
token_record&.iv || Gitlab::CryptoHelper::AES256_GCM_IV_STATIC
end
def insecure_strategy
@insecure_strategy ||= TokenAuthenticatableStrategies::Insecure
.new(klass, token_field, options)
......
# frozen_string_literal: true
# rubocop: todo Gitlab/NamespacedClass
class TokenWithIv < ApplicationRecord
validates :hashed_token, presence: true
validates :iv, presence: true
validates :hashed_plaintext_token, presence: true
def self.find_by_hashed_token(value)
find_by(hashed_token: ::Digest::SHA256.digest(value))
end
def self.find_by_plaintext_token(value)
find_by(hashed_plaintext_token: ::Digest::SHA256.digest(value))
end
def self.find_nonce_by_hashed_token(value)
return unless table_exists?
token_record = find_by_hashed_token(value)
token_record&.iv
end
end
---
title: Add token_with_iv table
merge_request:
author:
type: security
---
name: dynamic_nonce_creation
introduced_by_url:
rollout_issue_url:
milestone: '13.9'
type: development
group: group::manage
default_enabled: false
# frozen_string_literal: true
class CreateTokensWithIv < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :token_with_ivs do |t|
t.binary :hashed_token, null: false
t.binary :hashed_plaintext_token, null: false
t.binary :iv, null: false
t.index :hashed_token, name: 'index_token_with_ivs_on_hashed_token', unique: true, using: :btree
t.index :hashed_plaintext_token, name: 'index_token_with_ivs_on_hashed_plaintext_token', unique: true, using: :btree
end
end
end
......@@ -10,7 +10,7 @@ class EncryptFeatureFlagsClientsTokens < ActiveRecord::Migration[5.1]
def up
say_with_time("Encrypting tokens from operations_feature_flags_clients") do
FeatureFlagsClient.where('token_encrypted is NULL AND token IS NOT NULL').find_each do |feature_flags_client|
token_encrypted = Gitlab::CryptoHelper.aes256_gcm_encrypt(feature_flags_client.token)
token_encrypted = Gitlab::CryptoHelper.aes256_gcm_encrypt(feature_flags_client.token, nonce: Gitlab::CryptoHelper::AES256_GCM_IV_STATIC)
feature_flags_client.update!(token_encrypted: token_encrypted)
end
end
......
......@@ -10,7 +10,7 @@ class EncryptDeployTokensTokens < ActiveRecord::Migration[5.1]
def up
say_with_time("Encrypting tokens from deploy_tokens") do
DeploymentTokens.where('token_encrypted is NULL AND token IS NOT NULL').find_each(batch_size: 10000) do |deploy_token|
token_encrypted = Gitlab::CryptoHelper.aes256_gcm_encrypt(deploy_token.token)
token_encrypted = Gitlab::CryptoHelper.aes256_gcm_encrypt(deploy_token.token, nonce: Gitlab::CryptoHelper::AES256_GCM_IV_STATIC)
deploy_token.update!(token_encrypted: token_encrypted)
end
end
......
dde424c434c78e22087123fa30eec75c07268a9079fea44339915747aae235e0
\ No newline at end of file
......@@ -17402,6 +17402,22 @@ CREATE SEQUENCE todos_id_seq
ALTER SEQUENCE todos_id_seq OWNED BY todos.id;
CREATE TABLE token_with_ivs (
id bigint NOT NULL,
hashed_token bytea NOT NULL,
hashed_plaintext_token bytea NOT NULL,
iv bytea NOT NULL
);
CREATE SEQUENCE token_with_ivs_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE token_with_ivs_id_seq OWNED BY token_with_ivs.id;
CREATE TABLE trending_projects (
id integer NOT NULL,
project_id integer NOT NULL
......@@ -19119,6 +19135,8 @@ ALTER TABLE ONLY timelogs ALTER COLUMN id SET DEFAULT nextval('timelogs_id_seq':
ALTER TABLE ONLY todos ALTER COLUMN id SET DEFAULT nextval('todos_id_seq'::regclass);
ALTER TABLE ONLY token_with_ivs ALTER COLUMN id SET DEFAULT nextval('token_with_ivs_id_seq'::regclass);
ALTER TABLE ONLY trending_projects ALTER COLUMN id SET DEFAULT nextval('trending_projects_id_seq'::regclass);
ALTER TABLE ONLY u2f_registrations ALTER COLUMN id SET DEFAULT nextval('u2f_registrations_id_seq'::regclass);
......@@ -20641,6 +20659,9 @@ ALTER TABLE ONLY timelogs
ALTER TABLE ONLY todos
ADD CONSTRAINT todos_pkey PRIMARY KEY (id);
ALTER TABLE ONLY token_with_ivs
ADD CONSTRAINT token_with_ivs_pkey PRIMARY KEY (id);
ALTER TABLE ONLY trending_projects
ADD CONSTRAINT trending_projects_pkey PRIMARY KEY (id);
......@@ -23165,6 +23186,10 @@ CREATE INDEX index_todos_on_user_id_and_id_done ON todos USING btree (user_id, i
CREATE INDEX index_todos_on_user_id_and_id_pending ON todos USING btree (user_id, id) WHERE ((state)::text = 'pending'::text);
CREATE UNIQUE INDEX index_token_with_ivs_on_hashed_plaintext_token ON token_with_ivs USING btree (hashed_plaintext_token);
CREATE UNIQUE INDEX index_token_with_ivs_on_hashed_token ON token_with_ivs USING btree (hashed_token);
CREATE UNIQUE INDEX index_trending_projects_on_project_id ON trending_projects USING btree (project_id);
CREATE INDEX index_u2f_registrations_on_key_handle ON u2f_registrations USING btree (key_handle);
......
......@@ -23,7 +23,7 @@ RSpec.describe 'Geo read-only message', :geo do
context 'when in maintenance mode' do
before do
stub_application_setting(maintenance_mode: true)
stub_maintenance_mode_setting(true)
end
it_behaves_like 'Read-only instance', /This GitLab instance is undergoing maintenance and is operating in read\-only mode./
......
......@@ -22,7 +22,7 @@ RSpec.describe ApplicationHelper do
context 'maintenance mode' do
context 'enabled' do
before do
stub_application_setting(maintenance_mode: true)
stub_maintenance_mode_setting(true)
end
it 'returns default message' do
......@@ -48,7 +48,7 @@ RSpec.describe ApplicationHelper do
context 'disabled' do
it 'returns nil' do
stub_application_setting(maintenance_mode: false)
stub_maintenance_mode_setting(false)
expect(helper.read_only_message).to be_nil
end
......@@ -60,7 +60,7 @@ RSpec.describe ApplicationHelper do
context 'maintenance mode on' do
it 'returns messages for both' do
expect(Gitlab::Geo).to receive(:secondary?).twice { true }
stub_application_setting(maintenance_mode: true)
stub_maintenance_mode_setting(true)
expect(helper.read_only_message).to match(/you must visit the primary site/)
expect(helper.read_only_message).to match(/#{default_maintenance_mode_message}/)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::CryptoHelper do
include ::EE::GeoHelpers
describe '.read_only?' do
context 'with Geo enabled' do
before do
allow(Gitlab::Geo).to receive(:enabled?) { true }
allow(Gitlab::Geo).to receive(:current_node) { geo_node }
end
context 'is Geo secondary node' do
let(:geo_node) { create(:geo_node) }
it 'returns true' do
expect(described_class.read_only?).to be_truthy
end
end
context 'is Geo primary node' do
let(:geo_node) { create(:geo_node, :primary) }
it 'returns false when is Geo primary node' do
expect(described_class.read_only?).to be_falsey
end
end
end
end
end
......@@ -37,7 +37,7 @@ RSpec.describe Gitlab::Database do
context 'in maintenance mode' do
before do
stub_application_setting(maintenance_mode: true)
stub_maintenance_mode_setting(true)
end
it 'returns true' do
......
......@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Middleware::ReadOnly do
context 'when maintenance mode is on' do
before do
stub_application_setting(maintenance_mode: true)
stub_maintenance_mode_setting(true)
end
it_behaves_like 'write access for a read-only GitLab (EE) instance in maintenance mode'
......@@ -13,7 +13,7 @@ RSpec.describe Gitlab::Middleware::ReadOnly do
context 'when maintenance mode is not on' do
before do
stub_application_setting(maintenance_mode: false)
stub_maintenance_mode_setting(false)
end
it_behaves_like 'write access for a read-only GitLab (EE) instance'
......
......@@ -758,7 +758,7 @@ RSpec.describe Gitlab::GitAccess do
context 'when maintenance mode is enabled' do
before do
stub_application_setting(maintenance_mode: true)
stub_maintenance_mode_setting(true)
end
it 'blocks git push' do
......@@ -770,7 +770,7 @@ RSpec.describe Gitlab::GitAccess do
context 'when maintenance mode is disabled' do
before do
stub_application_setting(maintenance_mode: false)
stub_maintenance_mode_setting(false)
end
it 'allows git push' do
......
......@@ -12,8 +12,8 @@ RSpec.describe NullifyFeatureFlagPlaintextTokens do
let!(:project1) { projects.create!(namespace_id: namespace.id, name: 'Project 1') }
let!(:project2) { projects.create!(namespace_id: namespace.id, name: 'Project 2') }
let(:secret1_encrypted) { Gitlab::CryptoHelper.aes256_gcm_encrypt('secret1') }
let(:secret2_encrypted) { Gitlab::CryptoHelper.aes256_gcm_encrypt('secret2') }
let(:secret1_encrypted) { Gitlab::CryptoHelper.aes256_gcm_encrypt('secret1', nonce: Gitlab::CryptoHelper::AES256_GCM_IV_STATIC) }
let(:secret2_encrypted) { Gitlab::CryptoHelper.aes256_gcm_encrypt('secret2', nonce: Gitlab::CryptoHelper::AES256_GCM_IV_STATIC) }
before do
feature_flags_clients.create!(token: 'secret1', token_encrypted: secret1_encrypted, project_id: project1.id)
......
......@@ -248,7 +248,7 @@ RSpec.describe API::Internal::Base do
let_it_be(:project) { create(:project, :repository) }
before do
stub_application_setting(maintenance_mode: true)
stub_maintenance_mode_setting(true)
project.add_developer(user)
end
......
......@@ -19,7 +19,7 @@ RSpec.describe Auth::ContainerRegistryAuthenticationService do
end
before do
stub_application_setting(maintenance_mode: true)
stub_maintenance_mode_setting(true)
project.add_developer(current_user)
end
......
......@@ -7,7 +7,7 @@ RSpec.shared_examples 'write access for a read-only GitLab (EE) instance in main
include_context 'with a mocked GitLab instance'
before do
stub_application_setting(maintenance_mode: true)
stub_maintenance_mode_setting(true)
end
context 'normal requests to a read-only GitLab instance' do
......
......@@ -118,6 +118,7 @@ module Gitlab
def self.maintenance_mode?
return false unless ::Feature.enabled?(:maintenance_mode)
return false unless ::Gitlab::CurrentSettings.current_application_settings?
::Gitlab::CurrentSettings.maintenance_mode
end
......
......@@ -6,25 +6,44 @@ module Gitlab
AES256_GCM_OPTIONS = {
algorithm: 'aes-256-gcm',
key: Settings.attr_encrypted_db_key_base_32,
iv: Settings.attr_encrypted_db_key_base_12
key: Settings.attr_encrypted_db_key_base_32
}.freeze
AES256_GCM_IV_STATIC = Settings.attr_encrypted_db_key_base_12
def sha256(value)
salt = Settings.attr_encrypted_db_key_base_truncated
::Digest::SHA256.base64digest("#{value}#{salt}")
end
def aes256_gcm_encrypt(value)
encrypted_token = Encryptor.encrypt(AES256_GCM_OPTIONS.merge(value: value))
Base64.strict_encode64(encrypted_token)
def aes256_gcm_encrypt(value, nonce: nil)
aes256_gcm_encrypt_using_static_nonce(value)
end
def aes256_gcm_decrypt(value)
return unless value
nonce = Feature.enabled?(:dynamic_nonce_creation) ? dynamic_nonce(value) : AES256_GCM_IV_STATIC
encrypted_token = Base64.decode64(value)
Encryptor.decrypt(AES256_GCM_OPTIONS.merge(value: encrypted_token))
decrypted_token = Encryptor.decrypt(AES256_GCM_OPTIONS.merge(value: encrypted_token, iv: nonce))
decrypted_token
end
def dynamic_nonce(value)
TokenWithIv.find_nonce_by_hashed_token(value) || AES256_GCM_IV_STATIC
end
def aes256_gcm_encrypt_using_static_nonce(value)
create_encrypted_token(value, AES256_GCM_IV_STATIC)
end
def read_only?
Gitlab::Database.read_only?
end
def create_encrypted_token(value, iv)
encrypted_token = Encryptor.encrypt(AES256_GCM_OPTIONS.merge(value: value, iv: iv))
Base64.strict_encode64(encrypted_token)
end
end
end
......@@ -7,6 +7,10 @@ module Gitlab
Gitlab::SafeRequestStore.fetch(:current_application_settings) { ensure_application_settings! }
end
def current_application_settings?
Gitlab::SafeRequestStore.exist?(:current_application_settings) || ::ApplicationSetting.current.present?
end
def expire_current_application_settings
::ApplicationSetting.expire
Gitlab::SafeRequestStore.delete(:current_application_settings)
......
......@@ -27,7 +27,8 @@ RSpec.describe Admin::RunnersController do
# There is still an N+1 query for `runner.builds.count`
# We also need to add 1 because it takes 2 queries to preload tags
expect { get :index }.not_to exceed_query_limit(control_count + 6)
# also looking for token nonce requires database queries
expect { get :index }.not_to exceed_query_limit(control_count + 16)
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to have_content('tag1')
......
# frozen_string_literal: true
FactoryBot.define do
factory :token_with_iv do
hashed_token { ::Digest::SHA256.digest(SecureRandom.hex(50)) }
iv { ::Digest::SHA256.digest(SecureRandom.hex(50)) }
hashed_plaintext_token { ::Digest::SHA256.digest(SecureRandom.hex(50)) }
end
end
......@@ -19,21 +19,85 @@ RSpec.describe Gitlab::CryptoHelper do
expect(encrypted).to match %r{\A[A-Za-z0-9+/=]+\z}
expect(encrypted).not_to include "\n"
end
it 'does not save hashed token with iv value in database' do
expect { described_class.aes256_gcm_encrypt('some-value') }.not_to change { TokenWithIv.count }
end
it 'encrypts using static iv' do
expect(Encryptor).to receive(:encrypt).with(described_class::AES256_GCM_OPTIONS.merge(value: 'some-value', iv: described_class::AES256_GCM_IV_STATIC)).and_return('hashed_value')
described_class.aes256_gcm_encrypt('some-value')
end
end
describe '.aes256_gcm_decrypt' do
let(:encrypted) { described_class.aes256_gcm_encrypt('some-value') }
before do
stub_feature_flags(dynamic_nonce_creation: false)
end
context 'when token was encrypted using static nonce' do
let(:encrypted) { described_class.aes256_gcm_encrypt('some-value', nonce: described_class::AES256_GCM_IV_STATIC) }
it 'correctly decrypts encrypted string' do
decrypted = described_class.aes256_gcm_decrypt(encrypted)
expect(decrypted).to eq 'some-value'
end
it 'decrypts a value when it ends with a new line character' do
decrypted = described_class.aes256_gcm_decrypt(encrypted + "\n")
it 'correctly decrypts encrypted string' do
decrypted = described_class.aes256_gcm_decrypt(encrypted)
expect(decrypted).to eq 'some-value'
end
expect(decrypted).to eq 'some-value'
it 'does not save hashed token with iv value in database' do
expect { described_class.aes256_gcm_decrypt(encrypted) }.not_to change { TokenWithIv.count }
end
context 'with feature flag switched on' do
before do
stub_feature_flags(dynamic_nonce_creation: true)
end
it 'correctly decrypts encrypted string' do
decrypted = described_class.aes256_gcm_decrypt(encrypted)
expect(decrypted).to eq 'some-value'
end
end
end
it 'decrypts a value when it ends with a new line character' do
decrypted = described_class.aes256_gcm_decrypt(encrypted + "\n")
context 'when token was encrypted using random nonce' do
let(:value) { 'random-value' }
# for compatibility with tokens encrypted using dynamic nonce
let!(:encrypted) do
iv = create_nonce
encrypted_token = described_class.create_encrypted_token(value, iv)
TokenWithIv.create!(hashed_token: Digest::SHA256.digest(encrypted_token), hashed_plaintext_token: Digest::SHA256.digest(encrypted_token), iv: iv)
encrypted_token
end
before do
stub_feature_flags(dynamic_nonce_creation: true)
end
expect(decrypted).to eq 'some-value'
it 'correctly decrypts encrypted string' do
decrypted = described_class.aes256_gcm_decrypt(encrypted)
expect(decrypted).to eq value
end
it 'does not save hashed token with iv value in database' do
expect { described_class.aes256_gcm_decrypt(encrypted) }.not_to change { TokenWithIv.count }
end
end
end
def create_nonce
cipher = OpenSSL::Cipher.new('aes-256-gcm')
cipher.encrypt # Required before '#random_iv' can be called
cipher.random_iv # Ensures that the IV is the correct length respective to the algorithm used.
end
end
......@@ -194,4 +194,32 @@ RSpec.describe Gitlab::CurrentSettings do
end
end
end
describe '#current_application_settings?', :use_clean_rails_memory_store_caching do
before do
allow(Gitlab::CurrentSettings).to receive(:current_application_settings?).and_call_original
end
it 'returns true when settings exist' do
create(:application_setting,
home_page_url: 'http://mydomain.com',
signup_enabled: false)
expect(described_class.current_application_settings?).to eq(true)
end
it 'returns false when settings do not exist' do
expect(described_class.current_application_settings?).to eq(false)
end
context 'with cache', :request_store do
include_context 'with settings in cache'
it 'returns an in-memory ApplicationSetting object' do
expect(ApplicationSetting).not_to receive(:current)
expect(described_class.current_application_settings?).to eq(true)
end
end
end
end
......@@ -332,13 +332,13 @@ RSpec.describe Gitlab do
describe '.maintenance_mode?' do
it 'returns true when maintenance mode is enabled' do
stub_application_setting(maintenance_mode: true)
stub_maintenance_mode_setting(true)
expect(described_class.maintenance_mode?).to eq(true)
end
it 'returns false when maintenance mode is disabled' do
stub_application_setting(maintenance_mode: false)
stub_maintenance_mode_setting(false)
expect(described_class.maintenance_mode?).to eq(false)
end
......
......@@ -8,7 +8,7 @@ RSpec.describe EncryptFeatureFlagsClientsTokens do
let(:feature_flags_clients) { table(:operations_feature_flags_clients) }
let(:projects) { table(:projects) }
let(:plaintext) { "secret-token" }
let(:ciphertext) { Gitlab::CryptoHelper.aes256_gcm_encrypt(plaintext) }
let(:ciphertext) { Gitlab::CryptoHelper.aes256_gcm_encrypt(plaintext, nonce: Gitlab::CryptoHelper::AES256_GCM_IV_STATIC) }
describe '#up' do
it 'keeps plaintext token the same and populates token_encrypted if not present' do
......
......@@ -358,7 +358,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
it 'calls .destroy_sessions' do
expect(ActiveSession).to(
receive(:destroy_sessions)
.with(anything, user, [active_session.public_id, rack_session.public_id, rack_session.private_id]))
.with(anything, user, [encrypted_active_session_id, rack_session.public_id, rack_session.private_id]))
subject
end
......
......@@ -54,7 +54,7 @@ RSpec.describe ApplicationSetting, 'TokenAuthenticatable' do
it 'persists new token as an encrypted string' do
expect(subject).to eq settings.reload.runners_registration_token
expect(settings.read_attribute('runners_registration_token_encrypted'))
.to eq Gitlab::CryptoHelper.aes256_gcm_encrypt(subject)
.to eq Gitlab::CryptoHelper.aes256_gcm_encrypt(subject, nonce: Gitlab::CryptoHelper::AES256_GCM_IV_STATIC)
expect(settings).to be_persisted
end
......@@ -243,7 +243,7 @@ RSpec.describe Ci::Build, 'TokenAuthenticatable' do
it 'persists new token as an encrypted string' do
build.ensure_token!
encrypted = Gitlab::CryptoHelper.aes256_gcm_encrypt(build.token)
encrypted = Gitlab::CryptoHelper.aes256_gcm_encrypt(build.token, nonce: Gitlab::CryptoHelper::AES256_GCM_IV_STATIC)
expect(build.read_attribute('token_encrypted')).to eq encrypted
end
......
......@@ -68,6 +68,10 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
context 'when using optional strategy' do
let(:options) { { encrypted: :optional } }
before do
stub_feature_flags(dynamic_nonce_creation: false)
end
it 'returns decrypted token when an encrypted token is present' do
allow(instance).to receive(:read_attribute)
.with('some_field_encrypted')
......@@ -124,7 +128,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
it 'writes encrypted token and removes plaintext token and returns it' do
expect(instance).to receive(:[]=)
.with('some_field_encrypted', encrypted)
.with('some_field_encrypted', any_args)
expect(instance).to receive(:[]=)
.with('some_field', nil)
......@@ -137,7 +141,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
it 'writes encrypted token and writes plaintext token' do
expect(instance).to receive(:[]=)
.with('some_field_encrypted', encrypted)
.with('some_field_encrypted', any_args)
expect(instance).to receive(:[]=)
.with('some_field', 'my-value')
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe TokenWithIv do
describe 'validations' do
it { is_expected.to validate_presence_of :hashed_token }
it { is_expected.to validate_presence_of :iv }
it { is_expected.to validate_presence_of :hashed_plaintext_token }
end
describe '.find_by_hashed_token' do
it 'only includes matching record' do
matching_record = create(:token_with_iv, hashed_token: ::Digest::SHA256.digest('hashed-token'))
create(:token_with_iv)
expect(described_class.find_by_hashed_token('hashed-token')).to eq(matching_record)
end
end
describe '.find_by_plaintext_token' do
it 'only includes matching record' do
matching_record = create(:token_with_iv, hashed_plaintext_token: ::Digest::SHA256.digest('hashed-token'))
create(:token_with_iv)
expect(described_class.find_by_plaintext_token('hashed-token')).to eq(matching_record)
end
end
end
......@@ -282,6 +282,8 @@ RSpec.configure do |config|
current_user_mode.send(:user)&.admin?
end
end
allow(Gitlab::CurrentSettings).to receive(:current_application_settings?).and_return(false)
end
config.around(:example, :quarantine) do |example|
......
......@@ -121,6 +121,12 @@ module StubConfiguration
allow(::Gitlab.config.packages).to receive_messages(to_settings(messages))
end
def stub_maintenance_mode_setting(value)
allow(Gitlab::CurrentSettings).to receive(:current_application_settings?).and_return(true)
stub_application_setting(maintenance_mode: value)
end
private
# Modifies stubbed messages to also stub possible predicate versions
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment