diff options
Diffstat (limited to 'app/models')
-rw-r--r-- | app/models/application_setting.rb | 2 | ||||
-rw-r--r-- | app/models/ci/runner.rb | 9 | ||||
-rw-r--r-- | app/models/concerns/token_authenticatable.rb | 20 | ||||
-rw-r--r-- | app/models/concerns/token_authenticatable_strategies/base.rb | 29 | ||||
-rw-r--r-- | app/models/concerns/token_authenticatable_strategies/encrypted.rb | 74 | ||||
-rw-r--r-- | app/models/group.rb | 2 | ||||
-rw-r--r-- | app/models/project.rb | 2 |
7 files changed, 115 insertions, 23 deletions
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 207ffae873a..4319db42019 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -7,7 +7,7 @@ class ApplicationSetting < ActiveRecord::Base include IgnorableColumn include ChronicDurationAttribute - add_authentication_token_field :runners_registration_token + add_authentication_token_field :runners_registration_token, encrypted: true, fallback: true add_authentication_token_field :health_check_access_token DOMAIN_LIST_SEPARATOR = %r{\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 31330d0682e..a4645658c72 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -8,6 +8,9 @@ module Ci include RedisCacheable include ChronicDurationAttribute include FromUnion + include TokenAuthenticatable + + add_authentication_token_field :token, encrypted: true, fallback: true enum access_level: { not_protected: 0, @@ -39,7 +42,7 @@ module Ci has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build' - before_validation :set_default_values + before_save :ensure_token scope :active, -> { where(active: true) } scope :paused, -> { where(active: false) } @@ -145,10 +148,6 @@ module Ci end end - def set_default_values - self.token = SecureRandom.hex(15) if self.token.blank? - end - def assign_to(project, current_user = nil) if instance_type? self.runner_type = :project_type diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index 23a43aec677..ca5329a3615 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -9,24 +9,18 @@ module TokenAuthenticatable private # rubocop:disable Lint/UselessAccessModifier def add_authentication_token_field(token_field, options = {}) - @token_fields = [] unless @token_fields - unique = options.fetch(:unique, true) - - if @token_fields.include?(token_field) + if token_authenticatable_fields.include?(token_field) raise ArgumentError.new("#{token_field} already configured via add_authentication_token_field") end - @token_fields << token_field + token_authenticatable_fields.push(token_field) attr_accessor :cleartext_tokens - strategy = if options[:digest] - TokenAuthenticatableStrategies::Digest.new(self, token_field, options) - else - TokenAuthenticatableStrategies::Insecure.new(self, token_field, options) - end + strategy = TokenAuthenticatableStrategies::Base + .fabricate(self, token_field, options) - if unique + if options.fetch(:unique, true) define_singleton_method("find_by_#{token_field}") do |token| strategy.find_token_authenticatable(token) end @@ -54,5 +48,9 @@ module TokenAuthenticatable strategy.reset_token!(self) end end + + def token_authenticatable_fields + @token_authenticatable_fields ||= [] + end end end diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb index 413721d3e6c..4c63c0dd629 100644 --- a/app/models/concerns/token_authenticatable_strategies/base.rb +++ b/app/models/concerns/token_authenticatable_strategies/base.rb @@ -2,6 +2,8 @@ module TokenAuthenticatableStrategies class Base + attr_reader :klass, :token_field, :options + def initialize(klass, token_field, options) @klass = klass @token_field = token_field @@ -22,6 +24,7 @@ module TokenAuthenticatableStrategies def ensure_token(instance) write_new_token(instance) unless token_set?(instance) + get_token(instance) end # Returns a token, but only saves when the database is in read & write mode @@ -36,6 +39,28 @@ module TokenAuthenticatableStrategies instance.save! if Gitlab::Database.read_write? end + def fallback? + unless options[:fallback].in?([true, false, nil]) + raise ArgumentError, 'fallback: needs to be a boolean value!' + end + + options[:fallback] == true + end + + def self.fabricate(model, field, options) + if options[:digest] && options[:encrypted] + raise ArgumentError, 'Incompatible options set!' + end + + if options[:digest] + TokenAuthenticatableStrategies::Digest.new(model, field, options) + elsif options[:encrypted] + TokenAuthenticatableStrategies::Encrypted.new(model, field, options) + else + TokenAuthenticatableStrategies::Insecure.new(model, field, options) + end + end + protected def write_new_token(instance) @@ -65,9 +90,5 @@ module TokenAuthenticatableStrategies def token_set?(instance) raise NotImplementedError end - - def token_field_name - @token_field - end end end diff --git a/app/models/concerns/token_authenticatable_strategies/encrypted.rb b/app/models/concerns/token_authenticatable_strategies/encrypted.rb new file mode 100644 index 00000000000..c76cdc3bb90 --- /dev/null +++ b/app/models/concerns/token_authenticatable_strategies/encrypted.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module TokenAuthenticatableStrategies + class Encrypted < Base + def find_token_authenticatable(token, unscoped = false) + return unless token + + encrypted_value = Gitlab::CryptoHelper.aes256_gcm_encrypt(token) + token_authenticatable = relation(unscoped) + .find_by(encrypted_field => encrypted_value) + + if fallback? + token_authenticatable ||= fallback_strategy + .find_token_authenticatable(token) + end + + token_authenticatable + end + + def ensure_token(instance) + # TODO, tech debt, because some specs are testing migrations, but are still + # using factory bot to create resources, it might happen that a database + # schema does not have "#{token_name}_encrypted" field yet, however a bunch + # of models call `ensure_#{token_name}` in `before_save`. + # + # In that case we are using insecure strategy, but this should only happen + # in tests, because otherwise `encrypted_field` is going to exist. + # + # Another use case is when we are caching resources / columns, like we do + # in case of ApplicationSetting. + + return super if instance.has_attribute?(encrypted_field) + + if fallback? + fallback_strategy.ensure_token(instance) + else + raise ArgumentError, 'No fallback defined when encrypted field is missing!' + end + end + + def get_token(instance) + encrypted_token = instance.read_attribute(encrypted_field) + token = Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token) + + token || (fallback_strategy.get_token(instance) if fallback?) + end + + def set_token(instance, token) + raise ArgumentError unless token.present? + + instance[encrypted_field] = Gitlab::CryptoHelper.aes256_gcm_encrypt(token) + instance[token_field] = nil if fallback? + token + end + + protected + + def fallback_strategy + @fallback_strategy ||= TokenAuthenticatableStrategies::Insecure + .new(klass, token_field, options) + end + + def token_set?(instance) + raw_token = instance.read_attribute(encrypted_field) + raw_token ||= (fallback_strategy.get_token(instance) if fallback?) + + raw_token.present? + end + + def encrypted_field + @encrypted_field ||= "#{@token_field}_encrypted" + end + end +end diff --git a/app/models/group.rb b/app/models/group.rb index adb9169cfcd..e90b28bfa02 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -55,7 +55,7 @@ class Group < Namespace validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 } - add_authentication_token_field :runners_token + add_authentication_token_field :runners_token, encrypted: true, fallback: true after_create :post_create_hook after_destroy :post_destroy_hook diff --git a/app/models/project.rb b/app/models/project.rb index 185fd76cbbc..c6351e1c7fc 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -85,7 +85,7 @@ class Project < ActiveRecord::Base default_value_for :snippets_enabled, gitlab_config_features.snippets default_value_for :only_allow_merge_if_all_discussions_are_resolved, false - add_authentication_token_field :runners_token + add_authentication_token_field :runners_token, encrypted: true, fallback: true before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? } |