UX: Help users understand the meaning of each scope. (#10468)

This commit is contained in:
Roman Rizzi 2020-08-18 15:12:04 -03:00 committed by GitHub
parent 882b0aac19
commit 390615fbcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 136 additions and 11 deletions

View File

@ -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
}
});
}
}
});

View File

@ -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
}
});
}
}
});

View File

@ -37,12 +37,15 @@
{{/admin-form-row}}
{{#unless useGlobalKey}}
<div class="scopes-title">{{i18n "admin.api.scopes.title"}}</div>
<p>{{i18n "admin.api.scopes.description"}}</p>
{{#each-in scopes as |resource actions|}}
<table class="scopes-table">
<thead>
<tr>
<td><b>{{resource}}</b></td>
<td></td>
<td>{{i18n "admin.api.scopes.allowed_urls"}}</td>
<td>{{i18n "admin.api.scopes.optional_allowed_parameters"}}</td>
</tr>
</thead>
@ -50,7 +53,15 @@
{{#each actions as |act|}}
<tr>
<td>{{input type="checkbox" checked=act.selected}}</td>
<td><b>{{act.name}}</b></td>
<td>
<div class="scope-name">{{act.name}}</div>
<span class="scope-tooltip" data-tooltip={{i18n (concat "admin.api.scopes.descriptions." resource "." act.key)}}>
{{d-icon "question-circle"}}
</span>
</td>
<td>
{{d-button icon="link" action=(action "showURLs" act.urls) class="btn-info"}}
</td>
<td>
{{#each act.params as |p|}}
<div>

View File

@ -81,14 +81,14 @@
{{/admin-form-row}}
{{#if model.api_key_scopes.length}}
{{#admin-form-row label="admin.api.scopes.title"}}
{{/admin-form-row}}
<div class="scopes-title">{{i18n "admin.api.scopes.title"}}</div>
<table class="scopes-table">
<thead>
<tr>
<td>{{i18n "admin.api.scopes.resource"}}</td>
<td>{{i18n "admin.api.scopes.action"}}</td>
<td>{{i18n "admin.api.scopes.allowed_urls"}}</td>
<td>{{i18n "admin.api.scopes.allowed_parameters"}}</td>
</tr>
</thead>
@ -96,7 +96,17 @@
{{#each model.api_key_scopes as |scope|}}
<tr>
<td>{{scope.resource}}</td>
<td>{{scope.action}}</td>
<td>
{{scope.action}}
<span
class="scope-tooltip"
data-tooltip={{i18n (concat "admin.api.scopes.descriptions." scope.resource "." scope.key)}}>
{{d-icon "question-circle"}}
</span>
</td>
<td>
{{d-button icon="link" action=(action "showURLs" scope.urls) class="btn-info"}}
</td>
<td>
{{#each scope.parameters as |p|}}
<div>

View File

@ -0,0 +1,11 @@
{{#d-modal-body title="admin.api.scopes.allowed_urls"}}
<div>
<ul>
{{#each model.urls as |url|}}
<li>
<code>{{url}}</code>
</li>
{{/each}}
</ul>
</div>
{{/d-modal-body}}

View File

@ -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;

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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"