diff --git a/app/assets/javascripts/admin/controllers/admin-api-keys-new.js b/app/assets/javascripts/admin/controllers/admin-api-keys-new.js index bd9c4463485..f8590220144 100644 --- a/app/assets/javascripts/admin/controllers/admin-api-keys-new.js +++ b/app/assets/javascripts/admin/controllers/admin-api-keys-new.js @@ -3,6 +3,7 @@ import { isBlank } from "@ember/utils"; import Controller from "@ember/controller"; import discourseComputed from "discourse-common/utils/decorators"; import { popupAjaxError } from "discourse/lib/ajax-error"; +import showModal from "discourse/lib/show-modal"; export default Controller.extend({ userModes: [ @@ -48,6 +49,15 @@ export default Controller.extend({ continue() { this.transitionToRoute("adminApiKeys.show", this.model.id); + }, + + showURLs(urls) { + return showModal("admin-api-key-urls", { + admin: true, + model: { + urls + } + }); } } }); diff --git a/app/assets/javascripts/admin/controllers/admin-api-keys-show.js b/app/assets/javascripts/admin/controllers/admin-api-keys-show.js index 6927de87e7d..b907e51b09f 100644 --- a/app/assets/javascripts/admin/controllers/admin-api-keys-show.js +++ b/app/assets/javascripts/admin/controllers/admin-api-keys-show.js @@ -3,6 +3,7 @@ import Controller from "@ember/controller"; import { isEmpty } from "@ember/utils"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { empty } from "@ember/object/computed"; +import showModal from "discourse/lib/show-modal"; export default Controller.extend(bufferedProperty("model"), { isNew: empty("model.id"), @@ -51,6 +52,15 @@ export default Controller.extend(bufferedProperty("model"), { undoRevokeKey(key) { key.undoRevoke().catch(popupAjaxError); + }, + + showURLs(urls) { + return showModal("admin-api-key-urls", { + admin: true, + model: { + urls + } + }); } } }); diff --git a/app/assets/javascripts/admin/templates/api-keys-new.hbs b/app/assets/javascripts/admin/templates/api-keys-new.hbs index 28095418a98..59678c1342a 100644 --- a/app/assets/javascripts/admin/templates/api-keys-new.hbs +++ b/app/assets/javascripts/admin/templates/api-keys-new.hbs @@ -37,12 +37,15 @@ {{/admin-form-row}} {{#unless useGlobalKey}} +
{{i18n "admin.api.scopes.title"}}
+

{{i18n "admin.api.scopes.description"}}

{{#each-in scopes as |resource actions|}} + @@ -50,7 +53,15 @@ {{#each actions as |act|}} - + +
{{resource}} {{i18n "admin.api.scopes.allowed_urls"}} {{i18n "admin.api.scopes.optional_allowed_parameters"}}
{{input type="checkbox" checked=act.selected}}{{act.name}} +
{{act.name}}
+ + {{d-icon "question-circle"}} + +
+ {{d-button icon="link" action=(action "showURLs" act.urls) class="btn-info"}} + {{#each act.params as |p|}}
diff --git a/app/assets/javascripts/admin/templates/api-keys-show.hbs b/app/assets/javascripts/admin/templates/api-keys-show.hbs index 2ffd5c4e025..416771602c8 100644 --- a/app/assets/javascripts/admin/templates/api-keys-show.hbs +++ b/app/assets/javascripts/admin/templates/api-keys-show.hbs @@ -81,14 +81,14 @@ {{/admin-form-row}} {{#if model.api_key_scopes.length}} - {{#admin-form-row label="admin.api.scopes.title"}} - {{/admin-form-row}} +
{{i18n "admin.api.scopes.title"}}
+ @@ -96,7 +96,17 @@ {{#each model.api_key_scopes as |scope|}} - + +
{{i18n "admin.api.scopes.resource"}} {{i18n "admin.api.scopes.action"}}{{i18n "admin.api.scopes.allowed_urls"}} {{i18n "admin.api.scopes.allowed_parameters"}}
{{scope.resource}}{{scope.action}} + {{scope.action}} + + {{d-icon "question-circle"}} + + + {{d-button icon="link" action=(action "showURLs" scope.urls) class="btn-info"}} + {{#each scope.parameters as |p|}}
diff --git a/app/assets/javascripts/admin/templates/modal/admin-api-key-urls.hbs b/app/assets/javascripts/admin/templates/modal/admin-api-key-urls.hbs new file mode 100644 index 00000000000..f9a138f247b --- /dev/null +++ b/app/assets/javascripts/admin/templates/modal/admin-api-key-urls.hbs @@ -0,0 +1,11 @@ +{{#d-modal-body title="admin.api.scopes.allowed_urls"}} +
+
    + {{#each model.urls as |url|}} +
  • + {{url}} +
  • + {{/each}} +
+
+{{/d-modal-body}} diff --git a/app/assets/stylesheets/common/admin/api.scss b/app/assets/stylesheets/common/admin/api.scss index 3cfd1dc4006..2b42c0ac505 100644 --- a/app/assets/stylesheets/common/admin/api.scss +++ b/app/assets/stylesheets/common/admin/api.scss @@ -125,6 +125,20 @@ table.api-keys { text-align: left; width: 50%; } + .scopes-title { + font-size: $font-up-2; + font-weight: bold; + text-decoration: underline; + margin-top: 20px; + } + .scope-name { + font-weight: bold; + font-size: $font-0; + display: inline; + } + .scope-tooltip { + font-size: $font-down-1; + } } .scopes-table { margin: 20px 0 20px 0; diff --git a/app/controllers/admin/api_controller.rb b/app/controllers/admin/api_controller.rb index d2f8339bf78..7712cc308ef 100644 --- a/app/controllers/admin/api_controller.rb +++ b/app/controllers/admin/api_controller.rb @@ -25,7 +25,15 @@ class Admin::ApiController < Admin::AdminController def scopes scopes = ApiKeyScope.scope_mappings.reduce({}) do |memo, (resource, actions)| memo.tap do |m| - m[resource] = actions.map { |k, v| { id: "#{resource}:#{k}", name: k, params: v[:params] } } + m[resource] = actions.map do |k, v| + { + id: "#{resource}:#{k}", + key: k, + name: k.to_s.gsub('_', ' '), + params: v[:params], + urls: v[:urls] + } + end end end diff --git a/app/models/api_key_scope.rb b/app/models/api_key_scope.rb index 66fecc5a0e6..0e45ff698ae 100644 --- a/app/models/api_key_scope.rb +++ b/app/models/api_key_scope.rb @@ -6,7 +6,7 @@ class ApiKeyScope < ActiveRecord::Base class << self def list_actions - actions = [] + actions = %w[list#category_feed] TopTopic.periods.each do |p| actions.concat(["list#category_top_#{p}", "list#top_#{p}", "list#top_#{p}_feed"]) @@ -18,11 +18,20 @@ class ApiKeyScope < ActiveRecord::Base end def default_mappings - { + write_actions = %w[posts#create] + read_actions = %w[topics#show topics#feed] + + @default_mappings ||= { topics: { - write: { actions: %w[posts#create topics#feed], params: %i[topic_id] }, - read: { actions: %w[topics#show], 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 } } + write: { actions: write_actions, params: %i[topic_id], urls: find_urls(write_actions) }, + read: { + actions: read_actions, params: %i[topic_id], + aliases: { topic_id: :id }, urls: find_urls(read_actions) + }, + read_lists: { + actions: list_actions, params: %i[category_id], + aliases: { category_id: :category_slug_path_with_id }, urls: find_urls(list_actions) + } } } end @@ -32,10 +41,26 @@ class ApiKeyScope < ActiveRecord::Base default_mappings.tap do |mappings| plugin_mappings.each do |mapping| + mapping[:urls] = find_urls(mapping[:actions]) + mappings.deep_merge!(mapping) end end end + + def find_urls(actions) + Rails.application.routes.routes.reduce([]) do |memo, 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]&.match?('json') + excluded_paths = %w[/new-topic /new-message /exception] + + memo.tap do |m| + m << path if actions.include?(action) && api_supported_path && !excluded_paths.include?(path) + end + end + end end def permits?(route_param) diff --git a/app/serializers/api_key_scope_serializer.rb b/app/serializers/api_key_scope_serializer.rb index aa71a727472..f874efcf204 100644 --- a/app/serializers/api_key_scope_serializer.rb +++ b/app/serializers/api_key_scope_serializer.rb @@ -5,9 +5,23 @@ class ApiKeyScopeSerializer < ApplicationSerializer attributes :resource, :action, :parameters, - :allowed_parameters + :urls, + :allowed_parameters, + :key def parameters ApiKeyScope.scope_mappings.dig(object.resource.to_sym, object.action.to_sym, :params).to_a end + + def urls + ApiKeyScope.scope_mappings.dig(object.resource.to_sym, object.action.to_sym, :urls).to_a + end + + def action + object.action.to_s.gsub('_', ' ') + end + + def key + object.action + end end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 333cfd090d5..6ac1680edd1 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3646,12 +3646,24 @@ en: continue: Continue use_global_key: Global Key (allows all actions) scopes: + description: | + When using scopes, you can restrict an API key to a specific set of endpoints. + You can also define which parameters will be allowed. Use commas to separate multiple values. title: Scopes resource: Resource action: Action allowed_parameters: Allowed Parameters optional_allowed_parameters: Allowed Parameters (optional) any_parameter: (any parameter) + allowed_urls: Allowed URLs + descriptions: + topics: + read: | + Read a topic or a specific post in it. RSS is also supported. + write: | + Create a new topic or post to an existing one. + read_lists: | + Read topic lists like top, new, latest, etc. RSS is also supported. web_hooks: title: "Webhooks"