mirror of
https://github.com/discourse/discourse.git
synced 2024-11-30 19:05:18 +08:00
2f7a307237
It's possible in Rails to map a single route to multiple controller actions with different constraints. We do this in at least 1 place in our application for the root route (/) to make it possible to change the page that root route displays. This means that if you get the list of routes of your application, you'll get the same route for each time the route is defined. And if there's an API scope for 2 (or more) controller actions that map to the same route, the route will be listed twice in the Allowed URLs list of the scope. To prevent this, this PR adds the allowed URLs in a set so that duplicate routes are automatically removed.
174 lines
5.7 KiB
Ruby
174 lines
5.7 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class ApiKeyScope < ActiveRecord::Base
|
|
validates_presence_of :resource
|
|
validates_presence_of :action
|
|
|
|
class << self
|
|
def list_actions
|
|
actions = %w[list#category_feed]
|
|
|
|
%i[latest unread new top].each { |f| actions.concat(["list#category_#{f}", "list##{f}"]) }
|
|
|
|
actions
|
|
end
|
|
|
|
def default_mappings
|
|
return @default_mappings unless @default_mappings.nil?
|
|
|
|
mappings = {
|
|
global: {
|
|
read: { methods: %i[get] }
|
|
},
|
|
topics: {
|
|
write: { actions: %w[posts#create], params: %i[topic_id] },
|
|
update: { actions: %w[topics#update], params: %i[topic_id] },
|
|
read: {
|
|
actions: %w[topics#show topics#feed topics#posts],
|
|
params: %i[topic_id], aliases: { topic_id: :id }
|
|
},
|
|
read_lists: {
|
|
actions: list_actions, params: %i[category_id],
|
|
aliases: { category_id: :category_slug_path_with_id }
|
|
},
|
|
wordpress: { actions: %w[topics#wordpress], params: %i[topic_id] }
|
|
},
|
|
posts: {
|
|
edit: { actions: %w[posts#update], params: %i[id] }
|
|
},
|
|
categories: {
|
|
list: { actions: %w[categories#index] },
|
|
show: { actions: %w[categories#show], params: %i[id] }
|
|
},
|
|
uploads: {
|
|
create: {
|
|
actions: %w[
|
|
uploads#create
|
|
uploads#generate_presigned_put
|
|
uploads#complete_external_upload
|
|
uploads#create_multipart
|
|
uploads#batch_presign_multipart_parts
|
|
uploads#abort_multipart
|
|
uploads#complete_multipart
|
|
]
|
|
}
|
|
},
|
|
users: {
|
|
bookmarks: { actions: %w[users#bookmarks], params: %i[username] },
|
|
sync_sso: { actions: %w[admin/users#sync_sso], params: %i[sso sig] },
|
|
show: { actions: %w[users#show], params: %i[username external_id external_provider] },
|
|
check_emails: { actions: %w[users#check_emails], params: %i[username] },
|
|
update: { actions: %w[users#update], params: %i[username] },
|
|
log_out: { actions: %w[admin/users#log_out] },
|
|
anonymize: { actions: %w[admin/users#anonymize] },
|
|
delete: { actions: %w[admin/users#destroy] },
|
|
list: { actions: %w[admin/users#index] },
|
|
},
|
|
email: {
|
|
receive_emails: { actions: %w[admin/email#handle_mail] }
|
|
},
|
|
badges: {
|
|
create: { actions: %w[admin/badges#create] },
|
|
show: { actions: %w[badges#show] },
|
|
update: { actions: %w[admin/badges#update] },
|
|
delete: { actions: %w[admin/badges#destroy] },
|
|
list_user_badges: { actions: %w[user_badges#username], params: %i[username] },
|
|
assign_badge_to_user: { actions: %w[user_badges#create], params: %i[username] },
|
|
revoke_badge_from_user: { actions: %w[user_badges#destroy] },
|
|
}
|
|
}
|
|
|
|
parse_resources!(mappings)
|
|
@default_mappings = mappings
|
|
end
|
|
|
|
def scope_mappings
|
|
plugin_mappings = DiscoursePluginRegistry.api_key_scope_mappings
|
|
return default_mappings if plugin_mappings.empty?
|
|
|
|
default_mappings.deep_dup.tap do |mappings|
|
|
plugin_mappings.each do |plugin_mapping|
|
|
parse_resources!(plugin_mapping)
|
|
mappings.deep_merge!(plugin_mapping)
|
|
end
|
|
end
|
|
end
|
|
|
|
def parse_resources!(mappings)
|
|
mappings.each_value do |resource_actions|
|
|
resource_actions.each_value do |action_data|
|
|
action_data[:urls] = find_urls(actions: action_data[:actions], methods: action_data[:methods])
|
|
end
|
|
end
|
|
end
|
|
|
|
def find_urls(actions:, methods:)
|
|
urls = Set.new
|
|
|
|
if actions.present?
|
|
route_sets = [Rails.application.routes]
|
|
Rails::Engine.descendants.each do |engine|
|
|
next if engine == Rails::Application # abstract engine, can't call routes on it
|
|
next if engine == Discourse::Application # equiv. to Rails.application
|
|
route_sets << engine.routes
|
|
end
|
|
|
|
route_sets.each do |set|
|
|
engine_mount_path = set.find_script_name({}).presence
|
|
engine_mount_path = nil if engine_mount_path == "/"
|
|
set.routes.each do |route|
|
|
defaults = route.defaults
|
|
action = "#{defaults[:controller].to_s}##{defaults[:action]}"
|
|
path = route.path.spec.to_s.gsub(/\(\.:format\)/, '')
|
|
api_supported_path = (
|
|
path.end_with?('.rss') ||
|
|
!route.path.requirements[:format] ||
|
|
route.path.requirements[:format].match?('json')
|
|
)
|
|
excluded_paths = %w[/new-topic /new-message /exception]
|
|
|
|
if actions.include?(action) && api_supported_path && !excluded_paths.include?(path)
|
|
urls << "#{engine_mount_path}#{path} (#{route.verb})"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if methods.present?
|
|
methods.each do |method|
|
|
urls << "* (#{method})"
|
|
end
|
|
end
|
|
|
|
urls.to_a
|
|
end
|
|
end
|
|
|
|
def permits?(env)
|
|
RouteMatcher.new(**mapping.except(:urls), allowed_param_values: allowed_parameters).match?(env: env)
|
|
end
|
|
|
|
private
|
|
|
|
def mapping
|
|
@mapping ||= self.class.scope_mappings.dig(resource.to_sym, action.to_sym)
|
|
end
|
|
end
|
|
|
|
# == Schema Information
|
|
#
|
|
# Table name: api_key_scopes
|
|
#
|
|
# id :bigint not null, primary key
|
|
# api_key_id :integer not null
|
|
# resource :string not null
|
|
# action :string not null
|
|
# allowed_parameters :json
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
#
|
|
# Indexes
|
|
#
|
|
# index_api_key_scopes_on_api_key_id (api_key_id)
|
|
#
|