2019-05-03 06:17:27 +08:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2013-10-23 03:53:08 +08:00
|
|
|
class ApiKey < ActiveRecord::Base
|
2019-12-12 19:45:00 +08:00
|
|
|
class KeyAccessError < StandardError
|
|
|
|
end
|
|
|
|
|
2020-07-17 02:51:24 +08:00
|
|
|
has_many :api_key_scopes
|
2013-10-23 03:53:08 +08:00
|
|
|
belongs_to :user
|
2017-08-31 12:06:56 +08:00
|
|
|
belongs_to :created_by, class_name: "User"
|
2013-10-23 03:53:08 +08:00
|
|
|
|
2019-11-05 22:10:23 +08:00
|
|
|
scope :active, -> { where("revoked_at IS NULL") }
|
|
|
|
scope :revoked, -> { where("revoked_at IS NOT NULL") }
|
|
|
|
|
2019-12-12 19:45:00 +08:00
|
|
|
scope :with_key,
|
2023-11-29 13:38:07 +08:00
|
|
|
->(key) do
|
2019-12-12 19:45:00 +08:00
|
|
|
hashed = self.hash_key(key)
|
|
|
|
where(key_hash: hashed)
|
2023-11-29 13:38:07 +08:00
|
|
|
end
|
2013-10-23 03:53:08 +08:00
|
|
|
|
2023-05-15 12:12:25 +08:00
|
|
|
validates :description, length: { maximum: 255 }
|
|
|
|
|
2019-11-05 22:10:23 +08:00
|
|
|
after_initialize :generate_key
|
|
|
|
|
|
|
|
def generate_key
|
2019-12-12 19:45:00 +08:00
|
|
|
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"
|
2023-01-09 20:20:10 +08:00
|
|
|
end
|
2019-12-12 19:45:00 +08:00
|
|
|
@key
|
2013-10-23 03:53:08 +08:00
|
|
|
end
|
|
|
|
|
2019-12-12 19:45:00 +08:00
|
|
|
def key_available?
|
|
|
|
@key.present?
|
2019-11-05 22:10:23 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.last_used_epoch
|
|
|
|
SiteSetting.api_key_last_used_epoch.presence
|
2013-10-23 03:53:08 +08:00
|
|
|
end
|
|
|
|
|
2019-11-05 22:10:23 +08:00
|
|
|
def self.revoke_unused_keys!
|
2023-09-16 03:31:29 +08:00
|
|
|
return if SiteSetting.revoke_api_keys_unused_days == 0 # Never expire keys
|
2019-11-05 22:10:23 +08:00
|
|
|
to_revoke =
|
|
|
|
active.where(
|
|
|
|
"GREATEST(last_used_at, created_at, updated_at, :epoch) < :threshold",
|
|
|
|
epoch: last_used_epoch,
|
2023-09-16 03:31:29 +08:00
|
|
|
threshold: SiteSetting.revoke_api_keys_unused_days.days.ago,
|
2019-11-05 22:10:23 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
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",
|
2023-09-16 03:31:29 +08:00
|
|
|
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,
|
2023-01-09 20:20:10 +08:00
|
|
|
),
|
2019-11-05 22:10:23 +08:00
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2019-12-12 19:45:00 +08:00
|
|
|
|
|
|
|
def self.hash_key(key)
|
|
|
|
Digest::SHA256.hexdigest key
|
|
|
|
end
|
2020-07-17 02:51:24 +08:00
|
|
|
|
2020-10-07 00:20:15 +08:00
|
|
|
def request_allowed?(env)
|
|
|
|
if allowed_ips.present? && allowed_ips.none? { |ip| ip.include?(Rack::Request.new(env).ip) }
|
|
|
|
return false
|
2023-01-09 20:20:10 +08:00
|
|
|
end
|
2022-05-02 23:15:32 +08:00
|
|
|
return true if RouteMatcher.new(methods: :get, actions: "session#scopes").match?(env: env)
|
2020-07-17 02:51:24 +08:00
|
|
|
|
2020-10-07 00:20:15 +08:00
|
|
|
api_key_scopes.blank? || api_key_scopes.any? { |s| s.permits?(env) }
|
2020-07-17 02:51:24 +08:00
|
|
|
end
|
2022-04-06 22:01:52 +08:00
|
|
|
|
|
|
|
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
|
2013-10-23 03:53:08 +08:00
|
|
|
end
|
2013-12-05 14:40:35 +08:00
|
|
|
|
|
|
|
# == Schema Information
|
|
|
|
#
|
|
|
|
# Table name: api_keys
|
|
|
|
#
|
|
|
|
# id :integer not null, primary key
|
|
|
|
# user_id :integer
|
|
|
|
# created_by_id :integer
|
2014-08-27 13:19:25 +08:00
|
|
|
# created_at :datetime not null
|
|
|
|
# updated_at :datetime not null
|
2014-11-20 11:53:15 +08:00
|
|
|
# allowed_ips :inet is an Array
|
2014-12-24 17:11:41 +08:00
|
|
|
# hidden :boolean default(FALSE), not null
|
2019-09-03 16:10:29 +08:00
|
|
|
# last_used_at :datetime
|
2019-11-19 18:20:14 +08:00
|
|
|
# revoked_at :datetime
|
|
|
|
# description :text
|
2019-12-12 19:45:00 +08:00
|
|
|
# key_hash :string not null
|
|
|
|
# truncated_key :string not null
|
2013-12-05 14:40:35 +08:00
|
|
|
#
|
|
|
|
# Indexes
|
|
|
|
#
|
2019-12-12 19:45:00 +08:00
|
|
|
# index_api_keys_on_key_hash (key_hash)
|
|
|
|
# index_api_keys_on_user_id (user_id)
|
2013-12-05 14:40:35 +08:00
|
|
|
#
|