# frozen_string_literal: true class ApiKey < ActiveRecord::Base class KeyAccessError < StandardError end has_many :api_key_scopes belongs_to :user belongs_to :created_by, class_name: "User" scope :active, -> { where("revoked_at IS NULL") } scope :revoked, -> { where("revoked_at IS NOT NULL") } scope :with_key, ->(key) { hashed = self.hash_key(key) where(key_hash: hashed) } validates :description, length: { maximum: 255 } after_initialize :generate_key def generate_key if !self.key_hash @key ||= SecureRandom.hex(32) # Not saved to DB self.truncated_key = key[0..3] self.key_hash = ApiKey.hash_key(key) end end def key unless key_available? raise KeyAccessError.new "API key is only accessible immediately after creation" end @key end def key_available? @key.present? end def self.last_used_epoch SiteSetting.api_key_last_used_epoch.presence end def self.revoke_unused_keys! return if SiteSetting.revoke_api_keys_unused_days == 0 # Never expire keys to_revoke = active.where( "GREATEST(last_used_at, created_at, updated_at, :epoch) < :threshold", epoch: last_used_epoch, threshold: SiteSetting.revoke_api_keys_unused_days.days.ago, ) to_revoke.find_each do |api_key| ApiKey.transaction do api_key.update!(revoked_at: Time.zone.now) StaffActionLogger.new(Discourse.system_user).log_api_key( api_key, UserHistory.actions[:api_key_update], changes: api_key.saved_changes, context: I18n.t( "staff_action_logs.api_key.automatic_revoked", count: SiteSetting.revoke_api_keys_unused_days, ), ) end end end def self.revoke_max_life_keys! return if SiteSetting.revoke_api_keys_maxlife_days == 0 revoke_days_ago = SiteSetting.revoke_api_keys_maxlife_days.days.ago to_revoke = ApiKey.active.where("created_at < ?", revoke_days_ago) to_revoke.find_each do |api_key| ApiKey.transaction do api_key.update!(revoked_at: Time.zone.now) StaffActionLogger.new(Discourse.system_user).log_api_key( api_key, UserHistory.actions[:api_key_update], changes: api_key.saved_changes, context: I18n.t( "staff_action_logs.api_key.automatic_revoked_max_life", count: SiteSetting.revoke_api_keys_maxlife_days, ), ) end end end def self.hash_key(key) Digest::SHA256.hexdigest key end def request_allowed?(env) if allowed_ips.present? && allowed_ips.none? { |ip| ip.include?(Rack::Request.new(env).ip) } return false end return true if RouteMatcher.new(methods: :get, actions: "session#scopes").match?(env: env) api_key_scopes.blank? || api_key_scopes.any? { |s| s.permits?(env) } end def update_last_used!(now = Time.zone.now) return if last_used_at && (last_used_at > 1.minute.ago) # using update_column to avoid the AR transaction update_column(:last_used_at, now) end end # == Schema Information # # Table name: api_keys # # id :integer not null, primary key # user_id :integer # created_by_id :integer # created_at :datetime not null # updated_at :datetime not null # allowed_ips :inet is an Array # hidden :boolean default(FALSE), not null # last_used_at :datetime # revoked_at :datetime # description :text # key_hash :string not null # truncated_key :string not null # # Indexes # # index_api_keys_on_key_hash (key_hash) # index_api_keys_on_user_id (user_id) #