Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
99446d4
Api Tokenを要求できる関数を追加 (misskey-dev/misskey#10911)
chocolate-pie Jun 4, 2023
ba3ee6d
Merge remote-tracking branch 'upstream/develop' into flash-request-token
chocolate-pie Jun 4, 2023
694a6a3
:sparkles:
chocolate-pie Jun 5, 2023
08b6a20
Fix translate
chocolate-pie Jun 5, 2023
81eb257
Merge remote-tracking branch 'upstream/develop' into flash-request-token
chocolate-pie Jun 5, 2023
de7ec82
Merge branch 'develop' into flash-request-token
chocolate-pie Jun 6, 2023
7b63146
Fix Vulnerability
chocolate-pie Jun 7, 2023
38473c5
Merge remote-tracking branch 'upstream/develop' into flash-request-token
chocolate-pie Jun 7, 2023
1ebe572
Update CHANGELOG.md
chocolate-pie Jun 7, 2023
5ae91f5
Merge remote-tracking branch 'upstream/develop' into flash-request-token
chocolate-pie Jun 10, 2023
e045aa2
Merge remote-tracking branch 'upstream/develop' into flash-request-token
chocolate-pie Jun 11, 2023
0329b7a
Revert "Merge remote-tracking branch 'upstream/develop' into flash-re…
chocolate-pie Jun 11, 2023
f495296
Merge branch 'develop' into flash-request-token
chocolate-pie Jun 11, 2023
ab813c1
Merge branch 'develop' into flash-request-token
chocolate-pie Jun 21, 2023
876e9ab
Merge branch 'develop' into flash-request-token
chocolate-pie Jul 2, 2023
d42b15a
Update misskey-js.api.md
chocolate-pie Jul 2, 2023
8c3fd61
fix: Fix type error
chocolate-pie Jul 2, 2023
fe95950
Merge branch 'develop' into flash-request-token
chocolate-pie Jul 2, 2023
dbf4b52
Merge remote-tracking branch 'upstream/develop' into flash-request-token
chocolate-pie Jul 8, 2023
e4e8e70
refactor: いらない余白を削除
chocolate-pie Jul 8, 2023
e987077
Merge remote-tracking branch 'upstream/develop' into flash-request-token
chocolate-pie Jul 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@

-->

## 13.x.x (unreleased)

### General
- プレイにAPI Tokenを要求できる関数を追加

## 13.14.1

### General
Expand Down
3 changes: 3 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,9 @@ export interface Locale {
"additionalEmojiDictionary": string;
"installed": string;
"branding": string;
"additionalPermissionsForFlash": string;
"thisFlashRequiresTheFollowingPermissions": string;
"doYouWantToAllowThisPlayToAccessYourAccount": string;
"enableServerMachineStats": string;
"enableIdenticonGeneration": string;
"turnOffToImprovePerformance": string;
Expand Down
3 changes: 3 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,9 @@ goToMisskey: "Misskeyへ"
additionalEmojiDictionary: "絵文字の追加辞書"
installed: "インストール済み"
branding: "ブランディング"
additionalPermissionsForFlash: "Playへの追加許可"
thisFlashRequiresTheFollowingPermissions: "このPlayは以下の権限を要求しています"
doYouWantToAllowThisPlayToAccessYourAccount: "このPlayによるアカウントへのアクセスを許可しますか?"
enableServerMachineStats: "サーバーのマシン情報を公開する"
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
turnOffToImprovePerformance: "オフにするとパフォーマンスが向上します。"
Expand Down
9 changes: 9 additions & 0 deletions packages/backend/src/core/CacheService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js';
import type { FlashToken } from '@/misc/flash-token.js';
import type { OnApplicationShutdown } from '@nestjs/common';

@Injectable()
Expand All @@ -16,6 +17,7 @@ export class CacheService implements OnApplicationShutdown {
public localUserByIdCache: MemoryKVCache<LocalUser>;
public uriPersonCache: MemoryKVCache<User | null, string | null>;
public userProfileCache: RedisKVCache<UserProfile>;
public flashAccessTokensCache: RedisKVCache<FlashToken | null>;
public userMutingsCache: RedisKVCache<Set<string>>;
public userBlockingCache: RedisKVCache<Set<string>>;
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
Expand Down Expand Up @@ -147,6 +149,13 @@ export class CacheService implements OnApplicationShutdown {
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});

this.flashAccessTokensCache = new RedisKVCache<FlashToken | null>(this.redisClient, 'flashAccessTokens', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: async (key) => null,
toRedisConverter: (value) => JSON.stringify(value),
fromRedisConverter: (value) => JSON.parse(value),
});
this.redisForSub.on('message', this.onMessage);
}

Expand Down
6 changes: 6 additions & 0 deletions packages/backend/src/misc/flash-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { LocalUser } from '@/models/entities/User.js';

export type FlashToken = {
permissions: string[];
user: LocalUser
};
22 changes: 16 additions & 6 deletions packages/backend/src/server/api/ApiCallService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { AuthenticateService, AuthenticationError } from './AuthenticateService.
import type { FastifyRequest, FastifyReply } from 'fastify';
import type { OnApplicationShutdown } from '@nestjs/common';
import type { IEndpointMeta, IEndpoint } from './endpoints.js';
import type { FlashToken } from '@/misc/flash-token.js';

const pump = promisify(pipeline);

Expand Down Expand Up @@ -104,8 +105,8 @@ export class ApiCallService implements OnApplicationShutdown {
reply.code(400);
return;
}
this.authenticateService.authenticate(token).then(([user, app]) => {
this.call(endpoint, user, app, body, null, request).then((res) => {
this.authenticateService.authenticate(token).then(([user, app, flashToken]) => {
this.call(endpoint, user, app, flashToken, body, null, request).then((res) => {
if (request.method === 'GET' && endpoint.meta.cacheSec && !token && !user) {
reply.header('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`);
}
Expand Down Expand Up @@ -153,8 +154,8 @@ export class ApiCallService implements OnApplicationShutdown {
reply.code(400);
return;
}
this.authenticateService.authenticate(token).then(([user, app]) => {
this.call(endpoint, user, app, fields, {
this.authenticateService.authenticate(token).then(([user, app, flashToken]) => {
this.call(endpoint, user, app, flashToken, fields, {
name: multipartData.filename,
path: path,
}, request).then((res) => {
Expand Down Expand Up @@ -222,14 +223,15 @@ export class ApiCallService implements OnApplicationShutdown {
ep: IEndpoint & { exec: any },
user: LocalUser | null | undefined,
token: AccessToken | null | undefined,
flashToken: FlashToken | null | undefined,
data: any,
file: {
name: string;
path: string;
} | null,
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
) {
const isSecure = user != null && token == null;
const isSecure = user != null && token == null && flashToken == null;

if (ep.meta.secure && !isSecure) {
throw new ApiError(accessDenied);
Expand Down Expand Up @@ -336,6 +338,14 @@ export class ApiCallService implements OnApplicationShutdown {
});
}

if (flashToken && ep.meta.kind && !flashToken.permissions.some(p => p === ep.meta.kind)) {
throw new ApiError({
message: 'Your flash does not have the necessary permissions to use this endpoint.',
code: 'PERMISSION_DENIED',
id: '11924d17-113a-4ab0-954a-c567ee8a6ce5',
});
}

// Cast non JSON input
if ((ep.meta.requireFile || request.method === 'GET') && ep.params.properties) {
for (const k of Object.keys(ep.params.properties)) {
Expand All @@ -358,7 +368,7 @@ export class ApiCallService implements OnApplicationShutdown {
}

// API invoking
return await ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => {
return await ep.exec(data, user, token, flashToken, file, request.ip, request.headers).catch((err: Error) => {
if (err instanceof ApiError || err instanceof AuthenticationError) {
throw err;
} else {
Expand Down
18 changes: 12 additions & 6 deletions packages/backend/src/server/api/AuthenticateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { App } from '@/models/entities/App.js';
import { CacheService } from '@/core/CacheService.js';
import isNativeToken from '@/misc/is-native-token.js';
import { bindThis } from '@/decorators.js';
import type { FlashToken } from '@/misc/flash-token.js';

export class AuthenticationError extends Error {
constructor(message: string) {
Expand Down Expand Up @@ -36,9 +37,9 @@ export class AuthenticateService implements OnApplicationShutdown {
}

@bindThis
public async authenticate(token: string | null | undefined): Promise<[LocalUser | null, AccessToken | null]> {
public async authenticate(token: string | null | undefined): Promise<[LocalUser | null, AccessToken | null, FlashToken | null]> {
if (token == null) {
return [null, null];
return [null, null, null];
}

if (isNativeToken(token)) {
Expand All @@ -49,7 +50,7 @@ export class AuthenticateService implements OnApplicationShutdown {
throw new AuthenticationError('user not found');
}

return [user, null];
return [user, null, null];
} else {
const accessToken = await this.accessTokensRepository.findOne({
where: [{
Expand All @@ -60,7 +61,12 @@ export class AuthenticateService implements OnApplicationShutdown {
});

if (accessToken == null) {
throw new AuthenticationError('invalid signature');
const flashToken = await this.cacheService.flashAccessTokensCache.get(token);
if (flashToken !== null && typeof flashToken !== 'undefined') {
return [flashToken.user, null, flashToken];
} else {
throw new AuthenticationError('invalid signature');
}
}

this.accessTokensRepository.update(accessToken.id, {
Expand All @@ -79,9 +85,9 @@ export class AuthenticateService implements OnApplicationShutdown {
return [user, {
id: accessToken.id,
permission: app.permission,
} as AccessToken];
} as AccessToken, null];
} else {
return [user, accessToken];
return [user, accessToken, null];
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/server/api/EndpointsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ import * as ep___pages_update from './endpoints/pages/update.js';
import * as ep___flash_create from './endpoints/flash/create.js';
import * as ep___flash_delete from './endpoints/flash/delete.js';
import * as ep___flash_featured from './endpoints/flash/featured.js';
import * as ep___flash_genToken from './endpoints/flash/gen-token.js';
import * as ep___flash_like from './endpoints/flash/like.js';
import * as ep___flash_show from './endpoints/flash/show.js';
import * as ep___flash_unlike from './endpoints/flash/unlike.js';
Expand Down Expand Up @@ -634,6 +635,7 @@ const $pages_update: Provider = { provide: 'ep:pages/update', useClass: ep___pag
const $flash_create: Provider = { provide: 'ep:flash/create', useClass: ep___flash_create.default };
const $flash_delete: Provider = { provide: 'ep:flash/delete', useClass: ep___flash_delete.default };
const $flash_featured: Provider = { provide: 'ep:flash/featured', useClass: ep___flash_featured.default };
const $flash_genToken: Provider = { provide: 'ep:flash/gen-token', useClass: ep___flash_genToken.default };
const $flash_like: Provider = { provide: 'ep:flash/like', useClass: ep___flash_like.default };
const $flash_show: Provider = { provide: 'ep:flash/show', useClass: ep___flash_show.default };
const $flash_unlike: Provider = { provide: 'ep:flash/unlike', useClass: ep___flash_unlike.default };
Expand Down Expand Up @@ -983,6 +985,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$flash_create,
$flash_delete,
$flash_featured,
$flash_genToken,
$flash_like,
$flash_show,
$flash_unlike,
Expand Down Expand Up @@ -1326,6 +1329,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$flash_create,
$flash_delete,
$flash_featured,
$flash_genToken,
$flash_like,
$flash_show,
$flash_unlike,
Expand Down
9 changes: 5 additions & 4 deletions packages/backend/src/server/api/endpoint-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import _Ajv from 'ajv';
import type { Schema, SchemaType } from '@/misc/json-schema.js';
import type { LocalUser } from '@/models/entities/User.js';
import type { AccessToken } from '@/models/entities/AccessToken.js';
import type { FlashToken } from '@/misc/flash-token.js';
import { ApiError } from './error.js';
import type { IEndpointMeta } from './endpoints.js';

Expand All @@ -23,16 +24,16 @@ type File = {

// TODO: paramsの型をT['params']のスキーマ定義から推論する
type Executor<T extends IEndpointMeta, Ps extends Schema> =
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, flashToken: FlashToken | null, file?: File, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;

export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
public exec: (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => Promise<any>;
public exec: (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, flashToken: FlashToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => Promise<any>;

constructor(meta: T, paramDef: Ps, cb: Executor<T, Ps>) {
const validate = ajv.compile(paramDef);

this.exec = (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => {
this.exec = (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, flashToken: FlashToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => {
let cleanup: undefined | (() => void) = undefined;

if (meta.requireFile) {
Expand Down Expand Up @@ -63,7 +64,7 @@ export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
return Promise.reject(err);
}

return cb(params as SchemaType<Ps>, user, token, file, cleanup, ip, headers);
return cb(params as SchemaType<Ps>, user, token, flashToken, file, cleanup, ip, headers);
};
}
}
2 changes: 2 additions & 0 deletions packages/backend/src/server/api/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ import * as ep___pages_update from './endpoints/pages/update.js';
import * as ep___flash_create from './endpoints/flash/create.js';
import * as ep___flash_delete from './endpoints/flash/delete.js';
import * as ep___flash_featured from './endpoints/flash/featured.js';
import * as ep___flash_genToken from './endpoints/flash/gen-token.js';
import * as ep___flash_like from './endpoints/flash/like.js';
import * as ep___flash_show from './endpoints/flash/show.js';
import * as ep___flash_unlike from './endpoints/flash/unlike.js';
Expand Down Expand Up @@ -632,6 +633,7 @@ const eps = [
['flash/create', ep___flash_create],
['flash/delete', ep___flash_delete],
['flash/featured', ep___flash_featured],
['flash/gen-token', ep___flash_genToken],
['flash/like', ep___flash_like],
['flash/show', ep___flash_show],
['flash/unlike', ep___flash_unlike],
Expand Down
4 changes: 2 additions & 2 deletions packages/backend/src/server/api/endpoints/app/show.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {

private appEntityService: AppEntityService,
) {
super(meta, paramDef, async (ps, user, token) => {
const isSecure = user != null && token == null;
super(meta, paramDef, async (ps, user, token, flashToken) => {
const isSecure = user != null && token == null && flashToken == null;

// Lookup app
const ap = await this.appsRepository.findOneBy({ id: ps.appId });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private metaService: MetaService,
private driveService: DriveService,
) {
super(meta, paramDef, async (ps, me, _, file, cleanup, ip, headers) => {
super(meta, paramDef, async (ps, me, _1, _2, file, cleanup, ip, headers) => {
// Get 'name' parameter
let name = ps.name ?? file!.name ?? null;
if (name != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private driveService: DriveService,
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, user, _1, _2, _3, ip, headers) => {
super(meta, paramDef, async (ps, user, _1, _2, _3, _4, ip, headers) => {
this.driveService.uploadFromUrl({ url: ps.url, user, folderId: ps.folderId, sensitive: ps.isSensitive, force: ps.force, comment: ps.comment, requestIp: ip, requestHeaders: headers }).then(file => {
this.driveFileEntityService.pack(file, { self: true }).then(packedFile => {
this.globalEventService.publishMainStream(user.id, 'urlUploadFinished', {
Expand Down
56 changes: 56 additions & 0 deletions packages/backend/src/server/api/endpoints/flash/gen-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Injectable } from '@nestjs/common';
import ms from 'ms';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CacheService } from '@/core/CacheService.js';

export const meta = {
tags: ['flash'],

requireCredential: true,

prohibitMoved: true,

secure: true,

limit: {
duration: ms('1hour'),
max: 30,
},

res: {
type: 'object',
optional: false, nullable: false,
properties: {
token: { type: 'string' },
},
},
} as const;

export const paramDef = {
type: 'object',
properties: {
permissions: { type: 'array', items: {
type: 'string',
} },
},
required: ['permissions'],
} as const;

@Injectable() // eslint-disable-next-line import/no-default-export
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor (
private cacheService: CacheService,
) {
super(meta, paramDef, async (ps, me) => {
const token = secureRndstr(32);
await this.cacheService.flashAccessTokensCache.set(token, {
user: me,
permissions: ps.permissions,
});
return {
token,
};
});
}
}
4 changes: 2 additions & 2 deletions packages/backend/src/server/api/endpoints/i.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {

private userEntityService: UserEntityService,
) {
super(meta, paramDef, async (ps, user, token) => {
const isSecure = token == null;
super(meta, paramDef, async (ps, user, token, flashToken) => {
const isSecure = token == null && flashToken == null;

const now = new Date();
const today = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}`;
Expand Down
4 changes: 2 additions & 2 deletions packages/backend/src/server/api/endpoints/i/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private roleService: RoleService,
private cacheService: CacheService,
) {
super(meta, paramDef, async (ps, _user, token) => {
super(meta, paramDef, async (ps, _user, token, flashToken) => {
const user = await this.usersRepository.findOneByOrFail({ id: _user.id });
const isSecure = token == null;
const isSecure = token == null && flashToken == null;

const updates = {} as Partial<User>;
const profileUpdates = {} as Partial<UserProfile>;
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/server/api/endpoints/users/show.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private perUserPvChart: PerUserPvChart,
private apiLoggerService: ApiLoggerService,
) {
super(meta, paramDef, async (ps, me, _1, _2, _3, ip) => {
super(meta, paramDef, async (ps, me, _1, _2, _3, _4, ip) => {
let user;

const isModerator = await this.roleService.isModerator(me);
Expand Down
Loading