Loading app/models/concerns/token_authenticatable.rb +24 −31 Original line number Original line Diff line number Diff line Loading @@ -5,57 +5,50 @@ module TokenAuthenticatable private private def write_new_token(token_field) class_methods do new_token = generate_available_token(token_field) private # rubocop:disable Lint/UselessAccessModifier write_attribute(token_field, new_token) end def generate_available_token(token_field) def add_authentication_token_field(token_field, options = {}) loop do @token_fields = [] unless @token_fields token = generate_token(token_field) break token unless self.class.unscoped.find_by(token_field => token) end end def generate_token(token_field) if @token_fields.include?(token_field) Devise.friendly_token raise ArgumentError.new("#{token_field} already configured via add_authentication_token_field") end end class_methods do @token_fields << token_field def authentication_token_fields @token_fields || [] end private # rubocop:disable Lint/UselessAccessModifier attr_accessor :cleartext_tokens def add_authentication_token_field(token_field) strategy = if options[:digest] @token_fields = [] unless @token_fields TokenAuthenticatableStrategies::Digest.new(self, token_field, options) @token_fields << token_field else TokenAuthenticatableStrategies::Insecure.new(self, token_field, options) end define_singleton_method("find_by_#{token_field}") do |token| define_singleton_method("find_by_#{token_field}") do |token| find_by(token_field => token) if token strategy.find_token_authenticatable(token) end end define_method("ensure_#{token_field}") do define_method(token_field) do current_token = read_attribute(token_field) strategy.get_token(self) current_token.blank? ? write_new_token(token_field) : current_token end end define_method("set_#{token_field}") do |token| define_method("set_#{token_field}") do |token| write_attribute(token_field, token) if token strategy.set_token(self, token) end define_method("ensure_#{token_field}") do strategy.ensure_token(self) end end # Returns a token, but only saves when the database is in read & write mode # Returns a token, but only saves when the database is in read & write mode define_method("ensure_#{token_field}!") do define_method("ensure_#{token_field}!") do send("reset_#{token_field}!") if read_attribute(token_field).blank? # rubocop:disable GitlabSecurity/PublicSend strategy.ensure_token!(self) read_attribute(token_field) end end # Resets the token, but only saves when the database is in read & write mode # Resets the token, but only saves when the database is in read & write mode define_method("reset_#{token_field}!") do define_method("reset_#{token_field}!") do write_new_token(token_field) strategy.reset_token!(self) save! if Gitlab::Database.read_write? end end end end end end Loading app/models/concerns/token_authenticatable_strategies/base.rb 0 → 100644 +69 −0 Original line number Original line Diff line number Diff line # frozen_string_literal: true module TokenAuthenticatableStrategies class Base def initialize(klass, token_field, options) @klass = klass @token_field = token_field @options = options end def find_token_authenticatable(instance, unscoped = false) raise NotImplementedError end def get_token(instance) raise NotImplementedError end def set_token(instance) raise NotImplementedError end def ensure_token(instance) write_new_token(instance) unless token_set?(instance) end # Returns a token, but only saves when the database is in read & write mode def ensure_token!(instance) reset_token!(instance) unless token_set?(instance) get_token(instance) end # Resets the token, but only saves when the database is in read & write mode def reset_token!(instance) write_new_token(instance) instance.save! if Gitlab::Database.read_write? end protected def write_new_token(instance) new_token = generate_available_token set_token(instance, new_token) end def generate_available_token loop do token = generate_token break token unless find_token_authenticatable(token, true) end end def generate_token @options[:token_generator] ? @options[:token_generator].call : Devise.friendly_token end def relation(unscoped) unscoped ? @klass.unscoped : @klass end def token_set?(instance) raise NotImplementedError end def token_field_name @token_field end end end app/models/concerns/token_authenticatable_strategies/digest.rb 0 → 100644 +50 −0 Original line number Original line Diff line number Diff line # frozen_string_literal: true module TokenAuthenticatableStrategies class Digest < Base def find_token_authenticatable(token, unscoped = false) return unless token token_authenticatable = relation(unscoped).find_by(token_field_name => Gitlab::CryptoHelper.sha256(token)) if @options[:fallback] token_authenticatable ||= fallback_strategy.find_token_authenticatable(token) end token_authenticatable end def get_token(instance) token = instance.cleartext_tokens&.[](@token_field) token ||= fallback_strategy.get_token(instance) if @options[:fallback] token end def set_token(instance, token) return unless token instance.cleartext_tokens ||= {} instance.cleartext_tokens[@token_field] = token instance[token_field_name] = Gitlab::CryptoHelper.sha256(token) instance[@token_field] = nil if @options[:fallback] end protected def fallback_strategy @fallback_strategy ||= TokenAuthenticatableStrategies::Insecure.new(@klass, @token_field, @options) end def token_set?(instance) token_digest = instance.read_attribute(token_field_name) token_digest ||= instance.read_attribute(@token_field) if @options[:fallback] token_digest.present? end def token_field_name "#{@token_field}_digest" end end end app/models/concerns/token_authenticatable_strategies/insecure.rb 0 → 100644 +23 −0 Original line number Original line Diff line number Diff line # frozen_string_literal: true module TokenAuthenticatableStrategies class Insecure < Base def find_token_authenticatable(token, unscoped = false) relation(unscoped).find_by(@token_field => token) if token end def get_token(instance) instance.read_attribute(@token_field) end def set_token(instance, token) instance[@token_field] = token if token end protected def token_set?(instance) instance.read_attribute(@token_field).present? end end end app/models/personal_access_token.rb +11 −5 Original line number Original line Diff line number Diff line Loading @@ -3,7 +3,7 @@ class PersonalAccessToken < ActiveRecord::Base class PersonalAccessToken < ActiveRecord::Base include Expirable include Expirable include TokenAuthenticatable include TokenAuthenticatable add_authentication_token_field :token add_authentication_token_field :token, digest: true, fallback: true REDIS_EXPIRY_TIME = 3.minutes REDIS_EXPIRY_TIME = 3.minutes Loading Loading @@ -33,16 +33,22 @@ def active? def self.redis_getdel(user_id) def self.redis_getdel(user_id) Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis| token = redis.get(redis_shared_state_key(user_id)) encrypted_token = redis.get(redis_shared_state_key(user_id)) redis.del(redis_shared_state_key(user_id)) redis.del(redis_shared_state_key(user_id)) token begin Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token) rescue => ex logger.warn "Failed to decrypt PersonalAccessToken value stored in Redis for User ##{user_id}: #{ex.class}" encrypted_token end end end end end def self.redis_store!(user_id, token) def self.redis_store!(user_id, token) encrypted_token = Gitlab::CryptoHelper.aes256_gcm_encrypt(token) Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis| redis.set(redis_shared_state_key(user_id), token, ex: REDIS_EXPIRY_TIME) redis.set(redis_shared_state_key(user_id), encrypted_token, ex: REDIS_EXPIRY_TIME) token end end end end Loading Loading
app/models/concerns/token_authenticatable.rb +24 −31 Original line number Original line Diff line number Diff line Loading @@ -5,57 +5,50 @@ module TokenAuthenticatable private private def write_new_token(token_field) class_methods do new_token = generate_available_token(token_field) private # rubocop:disable Lint/UselessAccessModifier write_attribute(token_field, new_token) end def generate_available_token(token_field) def add_authentication_token_field(token_field, options = {}) loop do @token_fields = [] unless @token_fields token = generate_token(token_field) break token unless self.class.unscoped.find_by(token_field => token) end end def generate_token(token_field) if @token_fields.include?(token_field) Devise.friendly_token raise ArgumentError.new("#{token_field} already configured via add_authentication_token_field") end end class_methods do @token_fields << token_field def authentication_token_fields @token_fields || [] end private # rubocop:disable Lint/UselessAccessModifier attr_accessor :cleartext_tokens def add_authentication_token_field(token_field) strategy = if options[:digest] @token_fields = [] unless @token_fields TokenAuthenticatableStrategies::Digest.new(self, token_field, options) @token_fields << token_field else TokenAuthenticatableStrategies::Insecure.new(self, token_field, options) end define_singleton_method("find_by_#{token_field}") do |token| define_singleton_method("find_by_#{token_field}") do |token| find_by(token_field => token) if token strategy.find_token_authenticatable(token) end end define_method("ensure_#{token_field}") do define_method(token_field) do current_token = read_attribute(token_field) strategy.get_token(self) current_token.blank? ? write_new_token(token_field) : current_token end end define_method("set_#{token_field}") do |token| define_method("set_#{token_field}") do |token| write_attribute(token_field, token) if token strategy.set_token(self, token) end define_method("ensure_#{token_field}") do strategy.ensure_token(self) end end # Returns a token, but only saves when the database is in read & write mode # Returns a token, but only saves when the database is in read & write mode define_method("ensure_#{token_field}!") do define_method("ensure_#{token_field}!") do send("reset_#{token_field}!") if read_attribute(token_field).blank? # rubocop:disable GitlabSecurity/PublicSend strategy.ensure_token!(self) read_attribute(token_field) end end # Resets the token, but only saves when the database is in read & write mode # Resets the token, but only saves when the database is in read & write mode define_method("reset_#{token_field}!") do define_method("reset_#{token_field}!") do write_new_token(token_field) strategy.reset_token!(self) save! if Gitlab::Database.read_write? end end end end end end Loading
app/models/concerns/token_authenticatable_strategies/base.rb 0 → 100644 +69 −0 Original line number Original line Diff line number Diff line # frozen_string_literal: true module TokenAuthenticatableStrategies class Base def initialize(klass, token_field, options) @klass = klass @token_field = token_field @options = options end def find_token_authenticatable(instance, unscoped = false) raise NotImplementedError end def get_token(instance) raise NotImplementedError end def set_token(instance) raise NotImplementedError end def ensure_token(instance) write_new_token(instance) unless token_set?(instance) end # Returns a token, but only saves when the database is in read & write mode def ensure_token!(instance) reset_token!(instance) unless token_set?(instance) get_token(instance) end # Resets the token, but only saves when the database is in read & write mode def reset_token!(instance) write_new_token(instance) instance.save! if Gitlab::Database.read_write? end protected def write_new_token(instance) new_token = generate_available_token set_token(instance, new_token) end def generate_available_token loop do token = generate_token break token unless find_token_authenticatable(token, true) end end def generate_token @options[:token_generator] ? @options[:token_generator].call : Devise.friendly_token end def relation(unscoped) unscoped ? @klass.unscoped : @klass end def token_set?(instance) raise NotImplementedError end def token_field_name @token_field end end end
app/models/concerns/token_authenticatable_strategies/digest.rb 0 → 100644 +50 −0 Original line number Original line Diff line number Diff line # frozen_string_literal: true module TokenAuthenticatableStrategies class Digest < Base def find_token_authenticatable(token, unscoped = false) return unless token token_authenticatable = relation(unscoped).find_by(token_field_name => Gitlab::CryptoHelper.sha256(token)) if @options[:fallback] token_authenticatable ||= fallback_strategy.find_token_authenticatable(token) end token_authenticatable end def get_token(instance) token = instance.cleartext_tokens&.[](@token_field) token ||= fallback_strategy.get_token(instance) if @options[:fallback] token end def set_token(instance, token) return unless token instance.cleartext_tokens ||= {} instance.cleartext_tokens[@token_field] = token instance[token_field_name] = Gitlab::CryptoHelper.sha256(token) instance[@token_field] = nil if @options[:fallback] end protected def fallback_strategy @fallback_strategy ||= TokenAuthenticatableStrategies::Insecure.new(@klass, @token_field, @options) end def token_set?(instance) token_digest = instance.read_attribute(token_field_name) token_digest ||= instance.read_attribute(@token_field) if @options[:fallback] token_digest.present? end def token_field_name "#{@token_field}_digest" end end end
app/models/concerns/token_authenticatable_strategies/insecure.rb 0 → 100644 +23 −0 Original line number Original line Diff line number Diff line # frozen_string_literal: true module TokenAuthenticatableStrategies class Insecure < Base def find_token_authenticatable(token, unscoped = false) relation(unscoped).find_by(@token_field => token) if token end def get_token(instance) instance.read_attribute(@token_field) end def set_token(instance, token) instance[@token_field] = token if token end protected def token_set?(instance) instance.read_attribute(@token_field).present? end end end
app/models/personal_access_token.rb +11 −5 Original line number Original line Diff line number Diff line Loading @@ -3,7 +3,7 @@ class PersonalAccessToken < ActiveRecord::Base class PersonalAccessToken < ActiveRecord::Base include Expirable include Expirable include TokenAuthenticatable include TokenAuthenticatable add_authentication_token_field :token add_authentication_token_field :token, digest: true, fallback: true REDIS_EXPIRY_TIME = 3.minutes REDIS_EXPIRY_TIME = 3.minutes Loading Loading @@ -33,16 +33,22 @@ def active? def self.redis_getdel(user_id) def self.redis_getdel(user_id) Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis| token = redis.get(redis_shared_state_key(user_id)) encrypted_token = redis.get(redis_shared_state_key(user_id)) redis.del(redis_shared_state_key(user_id)) redis.del(redis_shared_state_key(user_id)) token begin Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token) rescue => ex logger.warn "Failed to decrypt PersonalAccessToken value stored in Redis for User ##{user_id}: #{ex.class}" encrypted_token end end end end end def self.redis_store!(user_id, token) def self.redis_store!(user_id, token) encrypted_token = Gitlab::CryptoHelper.aes256_gcm_encrypt(token) Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis| redis.set(redis_shared_state_key(user_id), token, ex: REDIS_EXPIRY_TIME) redis.set(redis_shared_state_key(user_id), encrypted_token, ex: REDIS_EXPIRY_TIME) token end end end end Loading