# frozen_string_literal: true class UserApiKey < ActiveRecord::Base self.ignored_columns = [ "scopes" # TODO(2020-12-18): remove ] SCOPES = { read: [:get], write: [:get, :post, :patch, :put, :delete], message_bus: [[:post, 'message_bus']], push: nil, one_time_password: nil, notifications: [[:post, 'message_bus'], [:get, 'notifications#index'], [:put, 'notifications#mark_read']], session_info: [ [:get, 'session#current'], [:get, 'users#topic_tracking_state'], [:get, 'list#unread'], [:get, 'list#new'], [:get, 'list#latest'] ] } belongs_to :user has_many :scopes, class_name: "UserApiKeyScope", dependent: :destroy scope :active, -> { where(revoked_at: nil) } scope :with_key, ->(key) { where(key_hash: ApiKey.hash_key(key)) } after_initialize :generate_key def generate_key if !self.key_hash @key ||= SecureRandom.hex self.key_hash = ApiKey.hash_key(@key) end end def key raise ApiKey::KeyAccessError.new "API key is only accessible immediately after creation" unless key_available? @key end def key_available? @key.present? end # Scopes allowed to be requested by external services def self.allowed_scopes Set.new(SiteSetting.allow_user_api_key_scopes.split("|")) end def self.available_scopes @available_scopes ||= Set.new(SCOPES.keys.map(&:to_s)) end def self.allow_permission?(permission, env) verb, action = permission actual_verb = env["REQUEST_METHOD"] || "" return false unless actual_verb.downcase == verb.to_s return true unless action # not a rails route, special handling return true if action == "message_bus" && env["PATH_INFO"] =~ /^\/message-bus\/.*\/poll/ params = env['action_dispatch.request.path_parameters'] return false unless params actual_action = "#{params[:controller]}##{params[:action]}" actual_action == action end def self.allow_scope?(name, env) if allowed = SCOPES[name.to_sym] good = allowed.any? do |permission| allow_permission?(permission, env) end good || allow_permission?([:post, 'user_api_keys#revoke'], env) end end def has_push? scopes.any? { |s| s.name == "push" || s.name == "notifications" } && push_url.present? && SiteSetting.allowed_user_api_push_urls.include?(push_url) end def allow?(env) scopes.any? do |s| UserApiKey.allow_scope?(s.name, env) end || is_revoke_self_request?(env) end def self.invalid_auth_redirect?(auth_redirect) SiteSetting.allowed_user_api_auth_redirects .split('|') .none? { |u| WildcardUrlChecker.check_url(u, auth_redirect) } end private def is_revoke_self_request?(env) UserApiKey.allow_permission?([:post, 'user_api_keys#revoke'], env) && (env[:id].nil? || env[:id].to_i == id) end end # == Schema Information # # Table name: user_api_keys # # id :integer not null, primary key # user_id :integer not null # client_id :string not null # application_name :string not null # push_url :string # created_at :datetime not null # updated_at :datetime not null # revoked_at :datetime # scopes :text default([]), not null, is an Array # last_used_at :datetime not null # key_hash :string not null # # Indexes # # index_user_api_keys_on_client_id (client_id) UNIQUE # index_user_api_keys_on_key_hash (key_hash) UNIQUE # index_user_api_keys_on_user_id (user_id) #