mirror of
https://github.com/we-promise/sure.git
synced 2026-04-07 22:34:47 +00:00
* feat: Protect demo monitoring API key from deletion - Add DEMO_MONITORING_KEY constant to ApiKey model - Add `demo_monitoring_key?` method to identify the monitoring key - Add `visible` scope to exclude monitoring key from UI queries - Update controller to use `visible` scope, hiding the monitoring key - Prevent revocation of the monitoring key with explicit error handling - Update Demo::Generator to use the shared constant Users on the demo instance can still create their own API keys, but cannot see or delete the monitoring key used for uptime checks. https://claude.ai/code/session_01RQFsw39K7PB5kztboVdBdB * Linter * Protect demo monitoring API key from deletion * Use monitoring source for demo API key * Add test for demo monitoring revoke guard * Disable Rack::Attack in test and development --------- Co-authored-by: Claude <noreply@anthropic.com>
119 lines
3.1 KiB
Ruby
119 lines
3.1 KiB
Ruby
class ApiKey < ApplicationRecord
|
|
include Encryptable
|
|
|
|
belongs_to :user
|
|
|
|
# Encrypt display_key if ActiveRecord encryption is configured
|
|
if encryption_ready?
|
|
encrypts :display_key, deterministic: true
|
|
end
|
|
|
|
# Constants
|
|
SOURCES = [ "web", "mobile", "monitoring" ].freeze
|
|
DEMO_MONITORING_KEY = "demo_monitoring_key_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
|
|
|
|
# Validations
|
|
validates :display_key, presence: true, uniqueness: true
|
|
validates :name, presence: true
|
|
validates :scopes, presence: true
|
|
validates :source, presence: true, inclusion: { in: SOURCES }
|
|
validate :scopes_not_empty
|
|
validate :one_active_key_per_user_per_source, on: :create
|
|
|
|
# Callbacks
|
|
before_validation :set_display_key
|
|
before_destroy :prevent_demo_monitoring_key_destroy!
|
|
|
|
# Scopes
|
|
scope :active, -> { where(revoked_at: nil).where("expires_at IS NULL OR expires_at > ?", Time.current) }
|
|
scope :visible, -> { where.not(display_key: DEMO_MONITORING_KEY) }
|
|
|
|
# Class methods
|
|
def self.find_by_value(plain_key)
|
|
return nil unless plain_key
|
|
|
|
# Find by encrypted display_key (deterministic encryption allows querying)
|
|
find_by(display_key: plain_key)&.tap do |api_key|
|
|
return api_key if api_key.active?
|
|
end
|
|
end
|
|
|
|
def self.generate_secure_key
|
|
SecureRandom.hex(32)
|
|
end
|
|
|
|
# Instance methods
|
|
def active?
|
|
!revoked? && !expired?
|
|
end
|
|
|
|
def revoked?
|
|
revoked_at.present?
|
|
end
|
|
|
|
def expired?
|
|
expires_at.present? && expires_at < Time.current
|
|
end
|
|
|
|
def key_matches?(plain_key)
|
|
display_key == plain_key
|
|
end
|
|
|
|
def revoke!
|
|
raise ActiveRecord::RecordNotDestroyed, "Cannot revoke demo monitoring API key" if demo_monitoring_key?
|
|
update!(revoked_at: Time.current)
|
|
end
|
|
|
|
def delete
|
|
raise ActiveRecord::RecordNotDestroyed, "Cannot destroy demo monitoring API key" if demo_monitoring_key?
|
|
super
|
|
end
|
|
|
|
def demo_monitoring_key?
|
|
display_key == DEMO_MONITORING_KEY
|
|
end
|
|
|
|
def update_last_used!
|
|
update_column(:last_used_at, Time.current)
|
|
end
|
|
|
|
# Get the plain text API key for display (automatically decrypted by Rails)
|
|
def plain_key
|
|
display_key
|
|
end
|
|
|
|
# Temporarily store the plain key for creation flow
|
|
attr_accessor :key
|
|
|
|
private
|
|
|
|
def set_display_key
|
|
if key.present?
|
|
self.display_key = key
|
|
end
|
|
end
|
|
|
|
def scopes_not_empty
|
|
if scopes.blank? || (scopes.is_a?(Array) && (scopes.empty? || scopes.all?(&:blank?)))
|
|
errors.add(:scopes, "must include at least one permission")
|
|
elsif scopes.is_a?(Array) && scopes.length > 1
|
|
errors.add(:scopes, "can only have one permission level")
|
|
elsif scopes.is_a?(Array) && !%w[read read_write].include?(scopes.first)
|
|
errors.add(:scopes, "must be either 'read' or 'read_write'")
|
|
end
|
|
end
|
|
|
|
def one_active_key_per_user_per_source
|
|
if user&.api_keys&.active&.where(source: source)&.where&.not(id: id)&.exists?
|
|
errors.add(:user, "can only have one active API key per source (#{source})")
|
|
end
|
|
end
|
|
|
|
def prevent_demo_monitoring_key_destroy!
|
|
return unless demo_monitoring_key?
|
|
|
|
errors.add(:base, "Cannot destroy demo monitoring API key")
|
|
throw(:abort)
|
|
end
|
|
end
|