From 1c684cbf938ef3cfc83c46a078f054fec2b3713f Mon Sep 17 00:00:00 2001 From: tamaina <tamaina@hotmail.co.jp> Date: Mon, 15 Apr 2019 12:10:40 +0900 Subject: [PATCH] Better permisson Fix #2341 (#4611) * Better permisson Fix #2341 * add kinds.ts * test * fix * v11 * fix --- locales/ja-JP.yml | 33 ++++++++--- src/client/app/dev/views/apps.vue | 2 +- src/client/app/dev/views/new-app.vue | 44 ++++++-------- src/prelude/array.ts | 5 ++ src/server/api/endpoints/i/favorites.ts | 2 +- src/server/api/endpoints/messaging/history.ts | 2 +- .../api/endpoints/messaging/messages.ts | 2 +- .../endpoints/messaging/messages/create.ts | 2 +- .../endpoints/messaging/messages/delete.ts | 2 +- .../api/endpoints/messaging/messages/read.ts | 2 +- .../api/endpoints/notes/favorites/create.ts | 2 +- .../api/endpoints/notes/favorites/delete.ts | 2 +- src/server/api/endpoints/notes/polls/vote.ts | 2 +- src/server/api/endpoints/permissions.ts | 29 ++++++++++ src/server/api/kinds.ts | 58 +++++++++++++++++++ src/server/api/openapi/description.ts | 20 ++++++- src/server/api/openapi/gen-spec.ts | 9 ++- test/api.ts | 12 +++- test/mfm.ts | 2 +- 19 files changed, 183 insertions(+), 49 deletions(-) create mode 100644 src/server/api/endpoints/permissions.ts create mode 100644 src/server/api/kinds.ts diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index cc6fe2b08..82115a722 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -74,10 +74,26 @@ common: favorites: "お気に入り" permissions: - 'read:account': "アカウントの情報を見る" - 'write:account': "アカウントの情報を変更する" - 'read:drive': "ドライブを見る" - 'write:drive': "ドライブを操作する" + "read:account": "アカウントの情報を見る" + "write:account": "アカウントの情報を変更する" + "read:blocks": "ブロックを見る" + "write:blocks": "ブロックを操作する" + "read:drive": "ドライブを見る" + "write:drive": "ドライブを操作する" + "read:favorites": "お気に入りを見る" + "write:favorites": "お気に入りを操作する" + "read:following": "フォローの情報を見る" + "write:following": "フォロー・フォロー解除する" + "read:messaging": "トークを見る" + "write:messaging": "トークを操作する" + "read:mutes": "ミュートを見る" + "write:mutes": "ミュートを操作する" + "write:notes": "投稿を作成・削除する" + "read:notifications": "通知を見る" + "write:notifications": "通知を操作する" + "read:reactions": "リアクションを見る" + "write:reactions": "リアクションを操作する" + "write:votes": "投票する" empty-timeline-info: follow-users-to-make-your-timeline: "ユーザーをフォローすると投稿がタイムラインに表示されます。" @@ -1804,14 +1820,17 @@ dev/views/apps.vue: app-missing: "アプリなし" dev/views/new-app.vue: + new-app: "新しいアプリケーション" + new-app-info: "アプリケーションはAPIからでも作成できます。 (app/create)" create-app: "アプリケーションの作成" app-name: "アプリケーション名" + app-name-placeholder: "ex) Misskey for iOS" app-name-desc: "あなたのアプリの名称。" - app-name-ex: "ex) Misskey for iOS" app-overview: "アプリの概要" - app-desc: "あなたのアプリの簡単な説明や紹介。" - app-desc-ex: "ex) Misskey iOSクライアント。" + app-overview-placeholder: " ex) Misskey iOSクライアント。" + app-overview-desc: "あなたのアプリの簡単な説明や紹介。" callback-url: "コールバックURL (オプション)" + callback-url-placeholder: "ex) https://your.app.example.com/callback.php" callback-url-desc: "ユーザーが認証フォームで認証した際にリダイレクトするURLを設定できます。" authority: "権限" authority-desc: "ここで要求した機能だけがAPIからアクセスできます。" diff --git a/src/client/app/dev/views/apps.vue b/src/client/app/dev/views/apps.vue index 78a7cede9..b99ccdf57 100644 --- a/src/client/app/dev/views/apps.vue +++ b/src/client/app/dev/views/apps.vue @@ -1,6 +1,6 @@ <template> <mk-ui> - <b-card :header="$t('header')"> + <b-card :header="$t('manage-apps')"> <b-button to="/app/new" variant="primary">{{ $t('create-app') }}</b-button> <hr> <div class="apps"> diff --git a/src/client/app/dev/views/new-app.vue b/src/client/app/dev/views/new-app.vue index 00f2ed60d..6b67d220a 100644 --- a/src/client/app/dev/views/new-app.vue +++ b/src/client/app/dev/views/new-app.vue @@ -1,35 +1,22 @@ <template> <mk-ui> - <b-card :header="$t('header')"> + <b-card :header="$t('new-app')"> + <b-alert show variant="info"><fa icon="info-circle"/> {{ $t('new-app-info') }}</b-alert> <b-form @submit.prevent="onSubmit" autocomplete="off"> - <b-form-group :label="$t('app-name')" :description="$t('description')"> - <b-form-input v-model="name" type="text" :placeholder="$t('placeholder')" autocomplete="off" required/> + <b-form-group :label="$t('app-name')" :description="$t('app-name-desc')"> + <b-form-input v-model="name" type="text" :placeholder="$t('app-name-placeholder')" autocomplete="off" required/> </b-form-group> - <b-form-group :label="$t('app-overview')" :description="$t('description')"> - <b-textarea v-model="description" :placeholder="$t('placeholder')" autocomplete="off" required></b-textarea> + <b-form-group :label="$t('app-overview')" :description="$t('app-overview-desc')"> + <b-textarea v-model="description" :placeholder="$t('app-overview-placeholder')" autocomplete="off" required></b-textarea> </b-form-group> - <b-form-group :label="$t('callback-url')" :description="$t('description')"> - <b-input v-model="cb" type="url" placeholder="ex) https://your.app.example.com/callback.php" autocomplete="off"/> + <b-form-group :label="$t('callback-url')" :description="$t('callback-url-desc')"> + <b-input v-model="cb" type="url" :placeholder="$t('callback-url-placeholder')" autocomplete="off"/> </b-form-group> - <b-card :header="$t('header')"> - <b-form-group :description="$t('description')"> + <b-card :header="$t('authority')"> + <b-form-group :description="$t('authority-desc')"> <b-alert show variant="warning"><fa icon="exclamation-triangle"/> {{ $t('authority-warning') }}</b-alert> <b-form-checkbox-group v-model="permission" stacked> - <b-form-checkbox value="read:account">{{ $t('read:account') }}</b-form-checkbox> - <b-form-checkbox value="write:account">{{ $t('write:account') }}</b-form-checkbox> - <b-form-checkbox value="write:notes">{{ $t('write:notes') }}</b-form-checkbox> - <b-form-checkbox value="read:reactions">{{ $t('read:reactions') }}</b-form-checkbox> - <b-form-checkbox value="write:reactions">{{ $t('write:reactions') }}</b-form-checkbox> - <b-form-checkbox value="read:following">{{ $t('read:following') }}</b-form-checkbox> - <b-form-checkbox value="write:following">{{ $t('write:following') }}</b-form-checkbox> - <b-form-checkbox value="read:mutes">{{ $t('read:mutes') }}</b-form-checkbox> - <b-form-checkbox value="write:mutes">{{ $t('write:mutes') }}</b-form-checkbox> - <b-form-checkbox value="read:blocks">{{ $t('read:blocks') }}</b-form-checkbox> - <b-form-checkbox value="write:blocks">{{ $t('write:blocks') }}</b-form-checkbox> - <b-form-checkbox value="read:drive">{{ $t('read:drive') }}</b-form-checkbox> - <b-form-checkbox value="write:drive">{{ $t('write:drive') }}</b-form-checkbox> - <b-form-checkbox value="read:notifications">{{ $t('read:notifications') }}</b-form-checkbox> - <b-form-checkbox value="write:notifications">{{ $t('write:notifications') }}</b-form-checkbox> + <b-form-checkbox v-for="v in permissionsList" :value="v" :key="v">{{ $t(`@.permissions.${v}`) }} ({{ v }})</b-form-checkbox> </b-form-checkbox-group> </b-form-group> </b-card> @@ -43,6 +30,7 @@ <script lang="ts"> import Vue from 'vue'; import i18n from '../../i18n'; + export default Vue.extend({ i18n: i18n('dev/views/new-app.vue'), data() { @@ -51,9 +39,15 @@ export default Vue.extend({ description: '', cb: '', nidState: null, - permission: [] + permission: [], + permissionsList: [] }; }, + created() { + this.$root.api('permissions').then(permissions => { + this.permissionsList = permissions + }); + }, methods: { onSubmit() { this.$root.api('app/create', { diff --git a/src/prelude/array.ts b/src/prelude/array.ts index 560dfa080..44482c57c 100644 --- a/src/prelude/array.ts +++ b/src/prelude/array.ts @@ -115,3 +115,8 @@ export function cumulativeSum(xs: number[]): number[] { for (let i = 1; i < ys.length; i++) ys[i] += ys[i - 1]; return ys; } + +// Object.fromEntries() +export function fromEntries(xs: [string, any][]): { [x: string]: any; } { + return xs.reduce((obj, [k, v]) => Object.assign(obj, { [k]: v }), {} as { [x: string]: any; }); +} diff --git a/src/server/api/endpoints/i/favorites.ts b/src/server/api/endpoints/i/favorites.ts index 2c25250be..aad706545 100644 --- a/src/server/api/endpoints/i/favorites.ts +++ b/src/server/api/endpoints/i/favorites.ts @@ -14,7 +14,7 @@ export const meta = { requireCredential: true, - kind: 'favorites-read', + kind: 'read:favorites', params: { limit: { diff --git a/src/server/api/endpoints/messaging/history.ts b/src/server/api/endpoints/messaging/history.ts index c12378eb7..c2d746e48 100644 --- a/src/server/api/endpoints/messaging/history.ts +++ b/src/server/api/endpoints/messaging/history.ts @@ -14,7 +14,7 @@ export const meta = { requireCredential: true, - kind: 'messaging-read', + kind: 'read:messaging', params: { limit: { diff --git a/src/server/api/endpoints/messaging/messages.ts b/src/server/api/endpoints/messaging/messages.ts index 02c57b8d0..add21e5f1 100644 --- a/src/server/api/endpoints/messaging/messages.ts +++ b/src/server/api/endpoints/messaging/messages.ts @@ -17,7 +17,7 @@ export const meta = { requireCredential: true, - kind: 'messaging-read', + kind: 'read:messaging', params: { userId: { diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts index 2c7e5ad2d..30ac0849a 100644 --- a/src/server/api/endpoints/messaging/messages/create.ts +++ b/src/server/api/endpoints/messaging/messages/create.ts @@ -20,7 +20,7 @@ export const meta = { requireCredential: true, - kind: 'messaging-write', + kind: 'write:messaging', params: { userId: { diff --git a/src/server/api/endpoints/messaging/messages/delete.ts b/src/server/api/endpoints/messaging/messages/delete.ts index 9f55caba6..6a896cd8d 100644 --- a/src/server/api/endpoints/messaging/messages/delete.ts +++ b/src/server/api/endpoints/messaging/messages/delete.ts @@ -18,7 +18,7 @@ export const meta = { requireCredential: true, - kind: 'messaging-write', + kind: 'write:messaging', limit: { duration: ms('1hour'), diff --git a/src/server/api/endpoints/messaging/messages/read.ts b/src/server/api/endpoints/messaging/messages/read.ts index 24a28285b..50b7f3987 100644 --- a/src/server/api/endpoints/messaging/messages/read.ts +++ b/src/server/api/endpoints/messaging/messages/read.ts @@ -15,7 +15,7 @@ export const meta = { requireCredential: true, - kind: 'messaging-write', + kind: 'write:messaging', params: { messageId: { diff --git a/src/server/api/endpoints/notes/favorites/create.ts b/src/server/api/endpoints/notes/favorites/create.ts index 7e0463775..bb0c9594b 100644 --- a/src/server/api/endpoints/notes/favorites/create.ts +++ b/src/server/api/endpoints/notes/favorites/create.ts @@ -18,7 +18,7 @@ export const meta = { requireCredential: true, - kind: 'favorite-write', + kind: 'write:favorites', params: { noteId: { diff --git a/src/server/api/endpoints/notes/favorites/delete.ts b/src/server/api/endpoints/notes/favorites/delete.ts index a889c84d4..49f763177 100644 --- a/src/server/api/endpoints/notes/favorites/delete.ts +++ b/src/server/api/endpoints/notes/favorites/delete.ts @@ -17,7 +17,7 @@ export const meta = { requireCredential: true, - kind: 'favorite-write', + kind: 'write:favorites', params: { noteId: { diff --git a/src/server/api/endpoints/notes/polls/vote.ts b/src/server/api/endpoints/notes/polls/vote.ts index e8b8b66da..d13405597 100644 --- a/src/server/api/endpoints/notes/polls/vote.ts +++ b/src/server/api/endpoints/notes/polls/vote.ts @@ -26,7 +26,7 @@ export const meta = { requireCredential: true, - kind: 'vote-write', + kind: 'write:votes', params: { noteId: { diff --git a/src/server/api/endpoints/permissions.ts b/src/server/api/endpoints/permissions.ts new file mode 100644 index 000000000..347e1e3f2 --- /dev/null +++ b/src/server/api/endpoints/permissions.ts @@ -0,0 +1,29 @@ +import define from '../define'; +import { kindsList } from '../kinds'; + +export const meta = { + stability: 'stable', + + desc: { + 'ja-JP': 'パーミッションの一覧を返します。', + 'en-US': 'Get the list of permissons.' + }, + + tags: ['meta'], + + requireCredential: false, + + params: { + }, + + res: { + type: 'array', + items: { + type: 'string', + } + }, +}; + +export default define(meta, async () => { + return kindsList; +}); diff --git a/src/server/api/kinds.ts b/src/server/api/kinds.ts new file mode 100644 index 000000000..d496fa691 --- /dev/null +++ b/src/server/api/kinds.ts @@ -0,0 +1,58 @@ +import endpoints from './endpoints'; +import * as locale from '../../../locales/'; +import { fromEntries } from '../../prelude/array'; + +export const kindsList = [ + 'read:account', + 'write:account', + 'read:blocks', + 'write:blocks', + 'read:drive', + 'write:drive', + 'read:favorites', + 'write:favorites', + 'read:following', + 'write:following', + 'read:messaging', + 'write:messaging', + 'read:mutes', + 'write:mutes', + 'write:notes', + 'read:notifications', + 'write:notifications', + 'read:reactions', + 'write:reactions', + 'write:votes' +]; + +export interface IKindInfo { + endpoints: string[]; + descs: { [x: string]: string; }; +} + +export function kinds() { + const kinds = fromEntries( + kindsList + .map(k => [k, { + endpoints: [], + descs: fromEntries( + Object.keys(locale) + .map(l => [l, locale[l].common.permissions[k] as string] as [string, string]) + ) as { [x: string]: string; } + }] as [ string, IKindInfo ]) + ) as { [x: string]: IKindInfo; }; + + const errors = [] as string[][]; + + for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) { + if (endpoint.meta.kind) { + const kind = endpoint.meta.kind; + if (kind in kinds) kinds[kind].endpoints.push(endpoint.name); + else errors.push([kind, endpoint.name]); + } + } + + if (errors.length > 0) throw Error('\n ' + errors.map((e) => `Unknown kind (permission) "${e[0]}" found at ${e[1]}.`).join('\n ')); + + return kinds; +} diff --git a/src/server/api/openapi/description.ts b/src/server/api/openapi/description.ts index 04a0b4c71..b801c8638 100644 --- a/src/server/api/openapi/description.ts +++ b/src/server/api/openapi/description.ts @@ -1,6 +1,14 @@ import config from '../../../config'; +import { IKindInfo, kinds } from '../kinds'; + +export function getDescription(lang = 'ja-JP'): string { + const permissionTable = (Object.entries(kinds()) as [string, IKindInfo][]) + .map(e => `|${e[0]}|${e[1].descs[lang]}|${e[1].endpoints.map(f => `[${f}](#operation/${f})`).join(', ')}|`) + .join('\n'); + + const descriptions = { + 'ja-JP': `**Misskey is a decentralized microblogging platform.** -export const description = ` ## Usage **APIはすべてPOSTでリクエスト/レスポンスともにJSON形式です。** 一部のAPIはリクエストに認証情報(APIキー)が必要です。リクエストの際に\`i\`というパラメータでAPIキーを添付してください。 @@ -44,4 +52,12 @@ APIキーの生成方法を擬似コードで表すと次のようになりま \`\`\` js const i = sha256(userToken + secretKey); \`\`\` -`; + +## Permissions +|Permisson (kind)|Description|Endpoints| +|:--|:--|:--| +${permissionTable} +` + } as { [x: string]: string }; + return lang in descriptions ? descriptions[lang] : descriptions['ja-JP']; +} diff --git a/src/server/api/openapi/gen-spec.ts b/src/server/api/openapi/gen-spec.ts index 915fb5a6a..d194c6c8a 100644 --- a/src/server/api/openapi/gen-spec.ts +++ b/src/server/api/openapi/gen-spec.ts @@ -3,7 +3,7 @@ import { Context } from 'cafy'; import config from '../../../config'; import { errors as basicErrors } from './errors'; import { schemas } from './schemas'; -import { description } from './description'; +import { getDescription } from './description'; import { convertOpenApiSchema } from '../../../misc/schema'; export function genOpenapiSpec(lang = 'ja-JP') { @@ -13,7 +13,7 @@ export function genOpenapiSpec(lang = 'ja-JP') { info: { version: 'v1', title: 'Misskey API', - description: '**Misskey is a decentralized microblogging platform.**\n\n' + description, + description: getDescription(lang), 'x-logo': { url: '/assets/api-doc.png' } }, @@ -110,7 +110,10 @@ export function genOpenapiSpec(lang = 'ja-JP') { let desc = (endpoint.meta.desc ? endpoint.meta.desc[lang] : 'No description provided.') + '\n\n'; desc += `**Credential required**: *${endpoint.meta.requireCredential ? 'Yes' : 'No'}*`; - if (endpoint.meta.kind) desc += ` / **Permission**: *${endpoint.meta.kind}*`; + if (endpoint.meta.kind) { + const kind = endpoint.meta.kind; + desc += ` / **Permission**: *${kind}*`; + } const info = { operationId: endpoint.name, diff --git a/test/api.ts b/test/api.ts index d14b28648..318aa8424 100644 --- a/test/api.ts +++ b/test/api.ts @@ -18,6 +18,8 @@ import * as assert from 'assert'; import * as childProcess from 'child_process'; import { async, signup, request, post, react, uploadFile } from './utils'; +import { kinds } from '../src/server/api/kinds'; + describe('API', () => { let p: childProcess.ChildProcess; @@ -792,7 +794,7 @@ describe('API', () => { parentId: folderA.id }, arisugawa); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('存在しない親フォルダを設定できない', async(async () => { @@ -965,5 +967,13 @@ describe('API', () => { assert.strictEqual(res.body[0].id, alicePost.id); })); }); + + describe('kinds', () => { + it('登録されていないパーミッションを利用しているAPIがない', () => { + const res = kinds(); + + assert.strictEqual(typeof res === 'object', true); + }); + }); }); */ diff --git a/test/mfm.ts b/test/mfm.ts index 69260a541..8098102e9 100644 --- a/test/mfm.ts +++ b/test/mfm.ts @@ -1141,7 +1141,7 @@ describe('MFM', () => { it('exlude emotes', () => { const tokens = parse('*.*'); assert.deepStrictEqual(tokens, [ - text("*.*"), + text('*.*'), ]); });