From afac97997767de61935d2960e928efbb75b3f12a Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Tue, 12 Nov 2024 02:18:25 -0600 Subject: [PATCH 1/2] Web security configuration Signed-off-by: eternal-flame-AD --- .config/cypress-devcontainer.yml | 22 +++++++++- .config/docker_example.yml | 22 +++++++++- .config/example.yml | 22 +++++++++- locales/en-US.yml | 1 + locales/ja-JP.yml | 1 + packages/backend/src/config.ts | 19 ++++++++- packages/backend/src/core/DriveService.ts | 40 +++++++++---------- packages/backend/src/server/ServerService.ts | 18 ++++++++- .../api/endpoints/admin/accounts/create.ts | 2 + packages/backend/src/server/csp.ts | 28 +++++++++++-- .../src/server/web/ClientServerService.ts | 16 +++++++- packages/misskey-js/etc/misskey-js.api.md | 2 +- .../misskey-js/src/autogen/apiClientJSDoc.ts | 2 +- packages/misskey-js/src/autogen/types.ts | 4 +- packages/misskey-js/src/consts.ts | 1 + 15 files changed, 163 insertions(+), 37 deletions(-) diff --git a/.config/cypress-devcontainer.yml b/.config/cypress-devcontainer.yml index e5bc1afa51..179ce77dc9 100644 --- a/.config/cypress-devcontainer.yml +++ b/.config/cypress-devcontainer.yml @@ -168,8 +168,8 @@ id: 'aidx' # options: # dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' -# ┌─────────────────────┐ -#───┘ Other configuration └───────────────────────────────────── +# ┌──────────────┐ +#──┘ Web Security └────────────────────────────────────── # Whether disable HSTS #disableHsts: true @@ -180,6 +180,24 @@ id: 'aidx' # - https://hstspreload.org/ #hstsPreload: false +# Enable additional security headers that reduce the risk of XSS attacks or privacy leaks. +# browserSandboxing: +# # Do not send the Referrer header to other domains. The default when browserSandboxing is missing is true. +# strictOriginReferrer: true +# csp: +# # Do not send a CSP header. The default is a strict CSP header that prevents any form of external fetching or execution. +# disable: false +# # Merge additional directives into the CSP header. The default is an empty object. +# # You may want to list your CDN or other trusted domains here. +# # Media proxies are automatically added to the CSP header. This is an exception, things like Sentry will not be automatically added. +# appendDirectives: +# 'script-src': +# - "'unsafe-eval'" # do not use this ... just an example +# - 'https://example.com' + +# ┌─────────────────────┐ +#───┘ Other configuration └───────────────────────────────────── + # Number of worker processes #clusterLimit: 1 diff --git a/.config/docker_example.yml b/.config/docker_example.yml index 7d51ab0443..35dfa5ded3 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -162,8 +162,8 @@ id: 'aidx' # options: # dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' -# ┌─────────────────────┐ -#───┘ Other configuration └───────────────────────────────────── +# ┌──────────────┐ +#──┘ Web Security └────────────────────────────────────── # Whether disable HSTS #disableHsts: true @@ -174,6 +174,24 @@ id: 'aidx' # - https://hstspreload.org/ #hstsPreload: false +# Enable additional security headers that reduce the risk of XSS attacks or privacy leaks. +# browserSandboxing: +# # Do not send the Referrer header to other domains. The default when browserSandboxing is missing is true. +# strictOriginReferrer: true +# csp: +# # Do not send a CSP header. The default is a strict CSP header that prevents any form of external fetching or execution. +# disable: false +# # Merge additional directives into the CSP header. The default is an empty object. +# # You may want to list your CDN or other trusted domains here. +# # Media proxies are automatically added to the CSP header. This is an exception, things like Sentry will not be automatically added. +# appendDirectives: +# 'script-src': +# - "'unsafe-eval'" # do not use this ... just an example +# - 'https://example.com' + +# ┌─────────────────────┐ +#───┘ Other configuration └───────────────────────────────────── + # Number of worker processes #clusterLimit: 1 diff --git a/.config/example.yml b/.config/example.yml index ce336d9f75..ef0e30d4e1 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -244,8 +244,8 @@ id: 'aidx' # options: # dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' -# ┌─────────────────────┐ -#───┘ Other configuration └───────────────────────────────────── +# ┌──────────────┐ +#──┘ Web Security └────────────────────────────────────── # Whether disable HSTS #disableHsts: true @@ -256,6 +256,24 @@ id: 'aidx' # - https://hstspreload.org/ #hstsPreload: false +# Enable additional security headers that reduce the risk of XSS attacks or privacy leaks. +# browserSandboxing: +# # Do not send the Referrer header to other domains. The default when browserSandboxing is missing is true. +# strictOriginReferrer: true +# csp: +# # Do not send a CSP header. The default is a strict CSP header that prevents any form of external fetching or execution. +# disable: false +# # Merge additional directives into the CSP header. The default is an empty object. +# # You may want to list your CDN or other trusted domains here. +# # Media proxies are automatically added to the CSP header. This is an exception, things like Sentry will not be automatically added. +# appendDirectives: +# 'script-src': +# - "'unsafe-eval'" # do not use this ... just an example +# - 'https://example.com' + +# ┌─────────────────────┐ +#───┘ Other configuration └───────────────────────────────────── + # Number of worker processes #clusterLimit: 1 diff --git a/locales/en-US.yml b/locales/en-US.yml index 8570addfa2..313126d669 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -2119,6 +2119,7 @@ _permissions: "read:flash-likes": "View list of liked Plays" "write:flash-likes": "Edit list of liked Plays" "read:admin:abuse-user-reports": "View user reports" + "write:admin:create-account": "Create user account" "write:admin:delete-account": "Delete user account" "write:admin:delete-all-files-of-a-user": "Delete all files of a user" "read:admin:index-stats": "View database index stats" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 5d8e1a5e72..fdc2ce045b 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2163,6 +2163,7 @@ _permissions: "read:flash-likes": "Playのいいねを見る" "write:flash-likes": "Playのいいねを操作する" "read:admin:abuse-user-reports": "ユーザーからの通報を見る" + "write:admin:create-account": "ユーザーアカウントを作成する" "write:admin:delete-account": "ユーザーアカウントを削除する" "write:admin:delete-all-files-of-a-user": "ユーザーのすべてのファイルを削除する" "read:admin:index-stats": "データベースインデックスに関する情報を見る" diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 0ac357be77..035c9c6331 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -20,6 +20,18 @@ type RedisOptionsSource = Partial & { prefix?: string; }; +type BrowserSandboxing = { + // send Referrer-Policy: strict-origin + strictOriginReferrer?: boolean; + csp?: { + disable?: boolean; + + appendDirectives?: { + [directive: string]: string | string[]; + } + }; +}; + /** * 設定ファイルの型 */ @@ -65,6 +77,8 @@ type Source = { publishTarballInsteadOfProvideRepositoryUrl?: boolean; + browserSandboxing?: BrowserSandboxing; + setupPassword?: string; proxy?: string; @@ -155,7 +169,9 @@ export type Config = { proxyRemoteFiles: boolean | undefined; signToActivityPubGet: boolean | undefined; - cspPrerenderedContent: Map, + browserSandboxing: BrowserSandboxing; + + cspPrerenderedContent: Map; version: string; gitDescribe: string; @@ -252,6 +268,7 @@ export function loadConfig(): Config { version, gitCommit, gitDescribe, + browserSandboxing: config.browserSandboxing ?? { strictOriginReferrer: true }, publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl, setupPassword: config.setupPassword, url: url.origin, diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index c332e5a0a8..495d67a93b 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -87,9 +87,9 @@ type UploadFromUrlArgs = { @Injectable() export class DriveService { - public static NoSuchFolderError = class extends Error {}; - public static InvalidFileNameError = class extends Error {}; - public static CannotUnmarkSensitiveError = class extends Error {}; + public static NoSuchFolderError = class extends Error { }; + public static InvalidFileNameError = class extends Error { }; + public static CannotUnmarkSensitiveError = class extends Error { }; private registerLogger: Logger; private downloaderLogger: Logger; private deleteLogger: Logger; @@ -147,11 +147,11 @@ export class DriveService { */ @bindThis private async save(file: MiDriveFile, path: string, name: string, type: string, hash: string, size: number): Promise { - // thunbnail, webpublic を必要なら生成 + // thunbnail, webpublic を必要なら生成 const alts = await this.generateAlts(path, type, !file.uri); if (this.meta.useObjectStorage) { - //#region ObjectStorage params + //#region ObjectStorage params let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) ?? ['']); if (ext === '') { @@ -170,11 +170,11 @@ export class DriveService { } const baseUrl = this.meta.objectStorageBaseUrl - ?? `${ this.meta.objectStorageUseSSL ? 'https' : 'http' }://${ this.meta.objectStorageEndpoint }${ this.meta.objectStoragePort ? `:${this.meta.objectStoragePort}` : '' }/${ this.meta.objectStorageBucket }`; + ?? `${this.meta.objectStorageUseSSL ? 'https' : 'http'}://${this.meta.objectStorageEndpoint}${this.meta.objectStoragePort ? `:${this.meta.objectStoragePort}` : ''}/${this.meta.objectStorageBucket}`; // for original const key = `${this.meta.objectStoragePrefix}/${randomUUID()}${ext}`; - const url = `${ baseUrl }/${ key }`; + const url = `${baseUrl}/${key}`; // for alts let webpublicKey: string | null = null; @@ -191,7 +191,7 @@ export class DriveService { if (alts.webpublic) { webpublicKey = `${this.meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`; - webpublicUrl = `${ baseUrl }/${ webpublicKey }`; + webpublicUrl = `${baseUrl}/${webpublicKey}`; this.registerLogger.info(`uploading webpublic: ${webpublicKey}`); uploads.push(this.upload(webpublicKey, alts.webpublic.data, alts.webpublic.type, alts.webpublic.ext, name)); @@ -199,7 +199,7 @@ export class DriveService { if (alts.thumbnail) { thumbnailKey = `${this.meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`; - thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`; + thumbnailUrl = `${baseUrl}/${thumbnailKey}`; this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`); uploads.push(this.upload(thumbnailKey, alts.thumbnail.data, alts.thumbnail.type, alts.thumbnail.ext, `${name}.thumbnail`)); @@ -311,9 +311,9 @@ export class DriveService { satisfyWebpublic = !!( type !== 'image/svg+xml' && // security reason type !== 'image/avif' && // not supported by Mastodon and MS Edge - !(metadata.exif ?? metadata.iptc ?? metadata.xmp ?? metadata.tifftagPhotoshop) && - metadata.width && metadata.width <= 2048 && - metadata.height && metadata.height <= 2048 + !(metadata.exif ?? metadata.iptc ?? metadata.xmp ?? metadata.tifftagPhotoshop) && + metadata.width && metadata.width <= 2048 && + metadata.height && metadata.height <= 2048 ); } catch (err) { this.registerLogger.warn(`sharp failed: ${err}`); @@ -470,11 +470,11 @@ export class DriveService { const info = await this.fileInfoService.getFileInfo(path, { skipSensitiveDetection: skipNsfwCheck, sensitiveThreshold: // 感度が高いほどしきい値は低くすることになる - this.meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 : - this.meta.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 : - this.meta.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 : - this.meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 : - 0.5, + this.meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 : + this.meta.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 : + this.meta.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 : + this.meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 : + 0.5, sensitiveThresholdForPorn: 0.75, enableSensitiveMediaDetectionForVideos: this.meta.enableSensitiveMediaDetectionForVideos, }); @@ -494,7 +494,7 @@ export class DriveService { ); if (user && !force) { - // Check if there is a file with the same hash + // Check if there is a file with the same hash const matched = await this.driveFilesRepository.findOneBy({ md5: info.md5, userId: user.id, @@ -582,7 +582,7 @@ export class DriveService { file.maybePorn = info.porn; file.isSensitive = user ? this.userEntityService.isLocalUser(user) && profile!.alwaysMarkNsfw ? true : - sensitive ?? false + sensitive ?? false : false; if (user && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) file.isSensitive = true; @@ -616,7 +616,7 @@ export class DriveService { file = await this.driveFilesRepository.insertOne(file); } catch (err) { - // duplicate key error (when already registered) + // duplicate key error (when already registered) if (isDuplicateKeyValueError(err)) { this.registerLogger.info(`already registered ${file.uri}`); diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index df67a728ef..be02274600 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -89,14 +89,28 @@ export class ServerService implements OnApplicationShutdown { fastify.addHook('onRequest', makeHstsHook(host, preload)); } + // Other Security/Privacy Headers + fastify.addHook('onRequest', (_, reply, done) => { + reply.header('x-content-type-options', 'nosniff'); + reply.header('permissions-policy', 'interest-cohort=()'); // Disable FLoC + if (this.config.browserSandboxing.strictOriginReferrer) { + reply.header('referrer-policy', 'strict-origin'); + } + done(); + }); + // CSP - if (process.env.NODE_ENV === 'production') { + if (process.env.NODE_ENV === 'production' && !this.config.browserSandboxing.csp?.disable) { console.debug('cspPrerenderedContent', this.config.cspPrerenderedContent); const generatedCSP = generateCSP(this.config.cspPrerenderedContent, { mediaProxy: this.config.mediaProxy ? `https://${new URL(this.config.mediaProxy).host}` : undefined, + script_src: [ + `https://${new URL(this.config.url).host}/embed_vite/`, + `https://${new URL(this.config.url).host}/vite/`, + ], }); fastify.addHook('onRequest', (_, reply, done) => { - reply.header('Content-Security-Policy', generatedCSP); + reply.header('content-security-policy', generatedCSP); done(); }); } diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts index d30131a62f..816dc4e04d 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts @@ -19,6 +19,8 @@ import { Packed } from '@/misc/json-schema.js'; export const meta = { tags: ['admin'], + kind: 'write:admin:create-account', + errors: { accessDenied: { message: 'Access denied.', diff --git a/packages/backend/src/server/csp.ts b/packages/backend/src/server/csp.ts index 45b37fbd06..aeee4eab3a 100644 --- a/packages/backend/src/server/csp.ts +++ b/packages/backend/src/server/csp.ts @@ -13,6 +13,8 @@ export type CSPHashed = { export function generateCSP(hashedMap: Map, options: { mediaProxy?: string, + script_src?: string[], + append?: { [key: string]: string | string[] }, }) { const keys = Array.from(hashedMap.keys()); const scripts = keys @@ -22,7 +24,7 @@ export function generateCSP(hashedMap: Map, options: { .filter(name => name.endsWith('.css')) .map(name => `'${hashedMap.get(name)!.integrity}'`); - return ([ + const cpolicy = [ ['default-src', ['\'self\'']], ['img-src', [ @@ -42,7 +44,11 @@ export function generateCSP(hashedMap: Map, options: { // // ref: https://github.com/shikijs/shiki/issues/671 ['style-src-attr', ['\'self\'', '\'unsafe-inline\'']], - ['script-src', ['\'self\'', '\'wasm-unsafe-eval\'', ...scripts]], + ['script-src', [ + ...(options.script_src ? options.script_src : ['\'self\'']), + '\'wasm-unsafe-eval\'', + ...scripts + ]], ['object-src', ['\'none\'']], ['base-uri', ['\'self\'']], ['form-action', ['\'self\'']], @@ -52,7 +58,23 @@ export function generateCSP(hashedMap: Map, options: { [ ['upgrade-insecure-requests', []], ] : []), - ] as [string, string[]][]) + ] as [string, string[]][]; + + if (options.append) { + for (const [name, values] of Object.entries(options.append)) { + if (!values) { + continue; + } + const found = cpolicy.find(([n]) => n === name); + if (found) { + found[1].push(...(Array.isArray(values) ? values : [values])); + } else { + cpolicy.push([name, Array.isArray(values) ? values : [values]]); + } + } + } + + return cpolicy .map(([name, values]) => { return `${name} ${values.join(' ')}`; }).join('; '); diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index e2f227323f..b107fdc7f9 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -248,14 +248,28 @@ export class ClientServerService { fastify.addHook('onRequest', makeHstsHook(host, preload)); } + // Other Security/Privacy Headers + fastify.addHook('onRequest', (_, reply, done) => { + reply.header('x-content-type-options', 'nosniff'); + reply.header('permissions-policy', 'interest-cohort=()'); // Disable FLoC + if (this.config.browserSandboxing.strictOriginReferrer ?? true) { + reply.header('referrer-policy', 'strict-origin'); + } + done(); + }); + // CSP if (process.env.NODE_ENV === 'production') { console.debug('cspPrerenderedContent', this.config.cspPrerenderedContent); const generatedCSP = generateCSP(this.config.cspPrerenderedContent, { mediaProxy: this.config.mediaProxy ? `https://${new URL(this.config.mediaProxy).host}` : undefined, + script_src: [ + `https://${new URL(this.config.url).host}/embed_vite/`, + `https://${new URL(this.config.url).host}/vite/`, + ], }); fastify.addHook('onRequest', (_, reply, done) => { - reply.header('Content-Security-Policy', generatedCSP); + reply.header('content-security-policy', generatedCSP); done(); }); } diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 8ac48678ed..2457783311 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -2876,7 +2876,7 @@ type PartialRolePolicyOverride = Partial<{ }>; // @public (undocumented) -export const permissions: readonly ["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", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"]; +export const permissions: readonly ["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", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:create-account", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"]; // @public (undocumented) type PingResponse = operations['ping']['responses']['200']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index e2c7cbba52..236eb87131 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -88,7 +88,7 @@ declare module '../api.js' { /** * No description provided. * - * **Credential required**: *No* + * **Credential required**: *No* / **Permission**: *write:admin:create-account* */ request( endpoint: E, diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 5f9b4316f3..227a9c5377 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -85,7 +85,7 @@ export type paths = { * admin/accounts/create * @description No description provided. * - * **Credential required**: *No* + * **Credential required**: *No* / **Permission**: *write:admin:create-account* */ post: operations['admin___accounts___create']; }; @@ -5659,7 +5659,7 @@ export type operations = { * admin/accounts/create * @description No description provided. * - * **Credential required**: *No* + * **Credential required**: *No* / **Permission**: *write:admin:create-account* */ admin___accounts___create: { requestBody: { diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index c5911a70eb..40cc44763f 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -64,6 +64,7 @@ export const permissions = [ 'read:flash-likes', 'write:flash-likes', 'read:admin:abuse-user-reports', + 'write:admin:create-account', 'write:admin:delete-account', 'write:admin:delete-all-files-of-a-user', 'read:admin:index-stats', From fd271ea268aa63c8f05f379869f51f1944a25b7a Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Thu, 14 Nov 2024 02:50:44 -0600 Subject: [PATCH 2/2] Relax admin automated account registration Signed-off-by: eternal-flame-AD --- .../api/endpoints/admin/accounts/create.ts | 8 +- .../backend/test/e2e/admin-create-account.ts | 88 +++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 packages/backend/test/e2e/admin-create-account.ts diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts index d30131a62f..29e1ddd5a0 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts @@ -15,18 +15,21 @@ import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { ApiError } from '@/server/api/error.js'; import { Packed } from '@/misc/json-schema.js'; +import { RoleService } from '@/core/RoleService.js'; export const meta = { tags: ['admin'], errors: { accessDenied: { + httpStatusCode: 403, message: 'Access denied.', code: 'ACCESS_DENIED', id: '1fb7cb09-d46a-4fff-b8df-057708cce513', }, wrongInitialPassword: { + httpStatusCode: 401, message: 'Initial password is incorrect.', code: 'INCORRECT_INITIAL_PASSWORD', id: '97147c55-1ae1-4f6f-91d6-e1c3e0e76d62', @@ -65,6 +68,7 @@ export default class extends Endpoint { // eslint- @Inject(DI.usersRepository) private usersRepository: UsersRepository, + private roleService: RoleService, private userEntityService: UserEntityService, private signupService: SignupService, private instanceActorService: InstanceActorService, @@ -85,8 +89,8 @@ export default class extends Endpoint { // eslint- // 初期パスワードが設定されていないのに初期パスワードが入力された場合 throw new ApiError(meta.errors.wrongInitialPassword); } - } else if ((realUsers && !me?.isRoot) || token !== null) { - // 初回セットアップではなく、管理者でない場合 or 外部トークンを使用している場合 + } else if (!(me?.isRoot) && !await this.roleService.isAdministrator(me)) { + // 管理者でない場合 throw new ApiError(meta.errors.accessDenied); } diff --git a/packages/backend/test/e2e/admin-create-account.ts b/packages/backend/test/e2e/admin-create-account.ts new file mode 100644 index 0000000000..357d624d3d --- /dev/null +++ b/packages/backend/test/e2e/admin-create-account.ts @@ -0,0 +1,88 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; + +import type * as misskey from 'misskey-js'; +import { api, role, signup } from '../utils.js'; + +describe('Admin Create User', () => { + let admin: misskey.entities.SignupResponse; + let user: misskey.entities.SignupResponse; + let formerAdmin: misskey.entities.SignupResponse; + let adminRole : misskey.entities.Role; + let formerAdminRole : misskey.entities.Role; + + beforeAll(async () => { + admin = await signup({ username: 'admin' }); + formerAdmin = await signup({ username: 'former_admin' }); + user = await signup({ username: 'user' }); + adminRole = await role(admin, { + name: 'admin', + isAdministrator: true + }); + formerAdminRole = await role(admin, { + name: 'former_admin', + isAdministrator: true + }); + const addAdminRole = await api('admin/roles/assign', { + userId: admin.id, + roleId: adminRole.id + }, admin); + assert.strictEqual(addAdminRole.status, 204); + + const addFormerAdminRole = await api('admin/roles/assign', { + userId: formerAdmin.id, + roleId: formerAdminRole.id + }, admin); + assert.strictEqual(addFormerAdminRole.status, 204); + }, 1000 * 60 * 2); + + test('Create User', async () => { + const newUser1 = await api('admin/accounts/create', { + username: 'new_user1', + password: 'password', + }, admin); + assert.strictEqual(newUser1.status, 200); + + const newUser2 = await api('admin/accounts/create', { + username: 'new_user2', + password: 'password', + }, formerAdmin); + assert.strictEqual(newUser2.status, 200); + + const newUser3 = await api('admin/accounts/create', { + username: 'new_user3', + password: 'password', + }, user); + assert.strictEqual(newUser3.status, 403); + }); + + test('Revoking Admin Role', async () => { + const res = await api('admin/roles/delete', {roleId: formerAdminRole.id}, admin); + assert.strictEqual(res.status, 200); + + const res2 = await api('admin/roles/delete', {roleId: adminRole.id}, formerAdmin); + assert.strictEqual(res2.status, 403); + }); + + test('Revoked User Should Not Create User', async () => { + const newUser4 = await api('admin/accounts/create', { + username: 'new_user4', + password: 'password', + }, formerAdmin); + + assert.strictEqual(newUser4.status, 403); + + const newUser5 = await api('admin/accounts/create', { + username: 'new_user5', + password: 'password', + }, admin); + + assert.strictEqual(newUser5.status, 200); + }); +}) \ No newline at end of file