diff --git a/app/models/user_api_key_scope.rb b/app/models/user_api_key_scope.rb index 60a4ca72dcb..11bd403dd58 100644 --- a/app/models/user_api_key_scope.rb +++ b/app/models/user_api_key_scope.rb @@ -17,7 +17,11 @@ class UserApiKeyScope < ActiveRecord::Base } def self.all_scopes - SCOPES + scopes = SCOPES + DiscoursePluginRegistry.user_api_key_scope_mappings.each do |mapping| + scopes = scopes.merge!(mapping) + end + scopes end def permits?(env) diff --git a/lib/discourse_plugin_registry.rb b/lib/discourse_plugin_registry.rb index 9e87e1374d9..39e00ca3a6e 100644 --- a/lib/discourse_plugin_registry.rb +++ b/lib/discourse_plugin_registry.rb @@ -81,6 +81,7 @@ class DiscoursePluginRegistry define_filtered_register :api_parameter_routes define_filtered_register :api_key_scope_mappings + define_filtered_register :user_api_key_scope_mappings define_filtered_register :permitted_bulk_action_parameters diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index 6745cbf22e7..0f0dc1d2291 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -793,6 +793,35 @@ class Plugin::Instance DiscoursePluginRegistry.register_api_key_scope_mapping({ resource => action }, self) end + # Register a new UserApiKey scope, and its allowed routes. Scope will be prefixed + # with the (parametetized) plugin name followed by a colon. + # + # For example, if discourse-awesome-plugin registered this: + # + # add_user_api_key_scope(:read_my_route, + # methods: :get, + # actions: "mycontroller#myaction", + # formats: :ics, + # parameters: :testparam + # ) + # + # The scope registered would be `discourse-awesome-plugin:read_my_route` + # + # Multiple matchers can be attached by supplying an array of parameter hashes + # + # See UserApiKeyScope::SCOPES for more examples + # And lib/route_matcher.rb for the route matching logic + def add_user_api_key_scope(scope_name, matcher_parameters) + raise ArgumentError.new("scope_name must be a symbol") if !scope_name.is_a?(Symbol) + matcher_parameters = [matcher_parameters] if !matcher_parameters.is_a?(Array) + + prefixed_scope_name = :"#{(name || directory_name).parameterize}:#{scope_name}" + DiscoursePluginRegistry.register_user_api_key_scope_mapping( + { + prefixed_scope_name => matcher_parameters&.map { |m| RouteMatcher.new(**m) } + }, self) + end + # Register a route which can be authenticated using an api key or user api key # in a query parameter rather than a header. For example: # diff --git a/spec/fabricators/user_api_key_fabricator.rb b/spec/fabricators/user_api_key_fabricator.rb index 5d9fe484398..3ca55431c8e 100644 --- a/spec/fabricators/user_api_key_fabricator.rb +++ b/spec/fabricators/user_api_key_fabricator.rb @@ -1,17 +1,17 @@ # frozen_string_literal: true +Fabricator(:user_api_key) do + user + client_id { SecureRandom.hex } + application_name 'some app' +end + Fabricator(:user_api_key_scope) Fabricator(:readonly_user_api_key, from: :user_api_key) do - user scopes { [Fabricate.build(:user_api_key_scope, name: 'read')] } - client_id { SecureRandom.hex } - application_name 'some app' end Fabricator(:bookmarks_calendar_user_api_key, from: :user_api_key) do - user scopes { [Fabricate.build(:user_api_key_scope, name: 'bookmarks_calendar')] } - client_id { SecureRandom.hex } - application_name 'some app' end diff --git a/spec/integration/api_keys_spec.rb b/spec/integration/api_keys_spec.rb index db6d9052b9c..0e8d7249b05 100644 --- a/spec/integration/api_keys_spec.rb +++ b/spec/integration/api_keys_spec.rb @@ -128,4 +128,24 @@ describe 'user api keys' do expect(response.status).to eq(200) # Can access own calendar end + context "with a plugin registered user api key scope" do + let(:user_api_key) { Fabricate(:user_api_key) } + + before do + metadata = Plugin::Metadata.new + metadata.name = "My Amazing Plugin" + plugin = Plugin::Instance.new metadata + plugin.add_user_api_key_scope :my_magic_scope, methods: :get, actions: "session#current" + user_api_key.scopes = [UserApiKeyScope.new(name: "my-amazing-plugin:my_magic_scope")] + user_api_key.save! + end + + it 'allows parameter access to the registered route' do + get '/session/current.json', headers: { + HTTP_USER_API_KEY: user_api_key.key + } + expect(response.status).to eq(200) + end + end + end