enhance(moderation): require 2FA to use moderator perms

This commit is contained in:
オスカー、 2024-08-13 15:48:57 +09:00
parent b546ab3252
commit a8d51ffe4c
Signed by: SWREI
GPG Key ID: 139D6573F92DA9F7
6 changed files with 49 additions and 14 deletions

4
locales/index.d.ts vendored
View File

@ -5159,6 +5159,10 @@ export interface Locale extends ILocale {
* *
*/ */
"mutualLink": string; "mutualLink": string;
/**
*
*/
"youNeedToEnableTwoFactor": string;
"_bubbleGame": { "_bubbleGame": {
/** /**
* *

View File

@ -1285,6 +1285,7 @@ refreshMetadata: "サーバー情報を更新"
removeAllFollowings: "相互フォロー解除" removeAllFollowings: "相互フォロー解除"
areYouSureToRemoveAllFollowings: "本当に{host}とのすべてのフォロー関係を削除しますか? 実行後は元に戻せません。 相手インスタンスが閉鎖されたと判断した場合のみ実行してください。" areYouSureToRemoveAllFollowings: "本当に{host}とのすべてのフォロー関係を削除しますか? 実行後は元に戻せません。 相手インスタンスが閉鎖されたと判断した場合のみ実行してください。"
mutualLink: "相互リンク" mutualLink: "相互リンク"
youNeedToEnableTwoFactor: "モデレーター権限を利用するには、まず二要素認証を有効にする必要があります。"
_bubbleGame: _bubbleGame:
howToPlay: "遊び方" howToPlay: "遊び方"

View File

@ -1270,6 +1270,7 @@ refreshMetadata: "서버 정보를 갱신하기"
removeAllFollowings: "모든 팔로우 관계를 제거하기" removeAllFollowings: "모든 팔로우 관계를 제거하기"
areYouSureToRemoveAllFollowings: "정말로 {host}와의 모든 팔로우 관계를 제거하시겠습니까? 실행한 후에는 되돌릴 수 없습니다. 상대 인스턴스가 폐쇄됐다고 판단되는 경우에만 실행하세요." areYouSureToRemoveAllFollowings: "정말로 {host}와의 모든 팔로우 관계를 제거하시겠습니까? 실행한 후에는 되돌릴 수 없습니다. 상대 인스턴스가 폐쇄됐다고 판단되는 경우에만 실행하세요."
mutualLink: "서로링크" mutualLink: "서로링크"
youNeedToEnableTwoFactor: "관리 권한을 이용하려면 먼저 2단계 인증을 활성화해야 합니다."
_bubbleGame: _bubbleGame:
howToPlay: "설명" howToPlay: "설명"
hold: "홀드" hold: "홀드"

View File

@ -17,6 +17,7 @@ import { MetaService } from '@/core/MetaService.js';
import { createTemp } from '@/misc/create-temp.js'; import { createTemp } from '@/misc/create-temp.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { ApiError } from './error.js'; import { ApiError } from './error.js';
@ -46,6 +47,7 @@ export class ApiCallService implements OnApplicationShutdown {
private userIpsRepository: UserIpsRepository, private userIpsRepository: UserIpsRepository,
private metaService: MetaService, private metaService: MetaService,
private userEntityService: UserEntityService,
private authenticateService: AuthenticateService, private authenticateService: AuthenticateService,
private rateLimiterService: RateLimiterService, private rateLimiterService: RateLimiterService,
private roleService: RoleService, private roleService: RoleService,
@ -218,12 +220,13 @@ export class ApiCallService implements OnApplicationShutdown {
} }
try { try {
this.userIpsRepository.createQueryBuilder().insert().values({ await this.userIpsRepository.createQueryBuilder().insert().values({
createdAt: new Date(), createdAt: new Date(),
userId: user.id, userId: user.id,
ip: ip, ip: ip,
}).orIgnore(true).execute(); }).orIgnore(true).execute();
} catch { } catch {
/* empty */
} }
} }
} }
@ -278,7 +281,7 @@ export class ApiCallService implements OnApplicationShutdown {
} }
} }
if (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin) { if (ep.meta.requireCredential ?? ep.meta.requireModerator ?? ep.meta.requireAdmin) {
if (user == null) { if (user == null) {
throw new ApiError({ throw new ApiError({
message: 'Credential required.', message: 'Credential required.',
@ -307,19 +310,39 @@ export class ApiCallService implements OnApplicationShutdown {
} }
} }
if ((ep.meta.requireModerator || ep.meta.requireAdmin) && !user!.isRoot) { if ((ep.meta.requireModerator ?? ep.meta.requireAdmin)) {
const myRoles = await this.roleService.getUserRoles(user!.id); const myRoles = await this.roleService.getUserRoles(user!.id);
if (ep.meta.requireModerator && !myRoles.some(r => r.isModerator || r.isAdministrator)) { const isModerator = myRoles.some(r => r.isModerator || r.isAdministrator);
const isAdmin = myRoles.some(r => r.isAdministrator);
const userProfile = await this.userEntityService.pack(user!.id, user, { schema: 'MeDetailed' });
const isMFAEnabled = userProfile.twoFactorEnabled;
if (!isMFAEnabled) {
throw new ApiError({ throw new ApiError({
message: 'You are not assigned to a moderator role.', message: 'You need to enable 2FA to access this endpoint.',
code: 'REQUIRES_MFA_ENABLED',
kind: 'permission',
id: 'abce13fe-1d9f-4e85-8f00-4a5251155470',
});
}
if (!user!.isRoot) {
throw new ApiError({
message: 'You are not assigned to a proper role.',
code: 'ROLE_PERMISSION_DENIED', code: 'ROLE_PERMISSION_DENIED',
kind: 'permission', kind: 'permission',
id: 'd33d5333-db36-423d-a8f9-1a2b9549da41', id: 'd33d5333-db36-423d-a8f9-1a2b9549da41',
}); });
} }
if (ep.meta.requireAdmin && !myRoles.some(r => r.isAdministrator)) { if (ep.meta.requireModerator && !isModerator) {
throw new ApiError({ throw new ApiError({
message: 'You are not assigned to an administrator role.', message: 'You are not assigned to a proper role.',
code: 'ROLE_PERMISSION_DENIED',
kind: 'permission',
id: 'd33d5333-db36-423d-a8f9-1a2b9549da41',
});
}
if (ep.meta.requireAdmin && !isAdmin) {
throw new ApiError({
message: 'You are not assigned to a proper role.',
code: 'ROLE_PERMISSION_DENIED', code: 'ROLE_PERMISSION_DENIED',
kind: 'permission', kind: 'permission',
id: 'c3d38592-54c0-429d-be96-5636b0431a61', id: 'c3d38592-54c0-429d-be96-5636b0431a61',
@ -332,7 +355,7 @@ export class ApiCallService implements OnApplicationShutdown {
const policies = await this.roleService.getUserPolicies(user!.id); const policies = await this.roleService.getUserPolicies(user!.id);
if (!policies[ep.meta.requireRolePolicy] && !myRoles.some(r => r.isAdministrator)) { if (!policies[ep.meta.requireRolePolicy] && !myRoles.some(r => r.isAdministrator)) {
throw new ApiError({ throw new ApiError({
message: 'You are not assigned to a required role.', message: 'Your role doesn\'t have proper permission.',
code: 'ROLE_PERMISSION_DENIED', code: 'ROLE_PERMISSION_DENIED',
kind: 'permission', kind: 'permission',
id: '7f86f06f-7e15-4057-8561-f4b6d4ac755a', id: '7f86f06f-7e15-4057-8561-f4b6d4ac755a',

View File

@ -25,11 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInfo> <MkInfo>
<Mfm :text="i18n.tsx._2fa.detailedGuide({ link: `[${i18n.ts.here}](https://go.misskey.io/howto-2fa)`})"/> <Mfm :text="i18n.tsx._2fa.detailedGuide({ link: `[${i18n.ts.here}](https://go.misskey.io/howto-2fa)`})"/>
</MkInfo> </MkInfo>
<MkInfo v-if="$i.securityKeysList.length > 0">{{ i18n.ts._2fa.whyTOTPOnlyRenew }}</MkInfo> <MkInfo v-if="$i.securityKeysList && $i.securityKeysList.length > 0">{{ i18n.ts._2fa.whyTOTPOnlyRenew }}</MkInfo>
<div v-if="$i.twoFactorEnabled" class="_gaps_s"> <div v-if="$i.twoFactorEnabled" class="_gaps_s">
<div v-text="i18n.ts._2fa.alreadyRegistered"/> <div v-text="i18n.ts._2fa.alreadyRegistered"/>
<MkButton v-if="$i.securityKeysList.length > 0" @click="renewTOTP">{{ i18n.ts._2fa.renewTOTP }}</MkButton> <MkButton v-if="$i.securityKeysList && $i.securityKeysList.length > 0" @click="renewTOTP">{{ i18n.ts._2fa.renewTOTP }}</MkButton>
<MkButton v-else danger @click="unregisterTOTP">{{ i18n.ts.unregister }}</MkButton> <MkButton v-else danger @click="unregisterTOTP">{{ i18n.ts.unregister }}</MkButton>
</div> </div>
@ -69,7 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</MkFolder> </MkFolder>
<MkSwitch :disabled="!$i.twoFactorEnabled || $i.securityKeysList.length === 0" :modelValue="usePasswordLessLogin" @update:modelValue="v => updatePasswordLessLogin(v)"> <MkSwitch :disabled="!$i.twoFactorEnabled || ($i.securityKeysList && $i.securityKeysList.length === 0)" :modelValue="usePasswordLessLogin" @update:modelValue="v => updatePasswordLessLogin(v)">
<template #label>{{ i18n.ts.passwordLessLogin }}</template> <template #label>{{ i18n.ts.passwordLessLogin }}</template>
<template #caption>{{ i18n.ts.passwordLessLoginDescription }}</template> <template #caption>{{ i18n.ts.passwordLessLoginDescription }}</template>
</MkSwitch> </MkSwitch>

View File

@ -17,14 +17,20 @@ SPDX-License-Identifier: AGPL-3.0-only
</span> </span>
<span :class="$style.title">{{ i18n.ts.verificationEmailSent }}</span> <span :class="$style.title">{{ i18n.ts.verificationEmailSent }}</span>
</MkA> </MkA>
<MkA v-if="unresolvedReportCount > 0" :class="$style.item" to="/admin/"> <MkA v-if="unresolvedReportCount > 0" :class="$style.item" to="/admin/abuses">
<span :class="$style.icon"> <span :class="$style.icon">
<i class="ti ti-circle-x" style="color: var(--error);"></i> <i class="ti ti-alert-triangle" style="color: var(--warn);"></i>
</span> </span>
<span :class="$style.title">{{ i18n.tsx.thereIsUnresolvedAbuseReport({ left: unresolvedReportCount }) }}</span> <span :class="$style.title">{{ i18n.tsx.thereIsUnresolvedAbuseReport({ left: unresolvedReportCount }) }}</span>
</MkA> </MkA>
<MkA v-if="($i?.isModerator ?? $i?.isAdmin) && !$i?.twoFactorEnabled" :class="$style.item" to="/settings/security">
<span :class="$style.icon">
<i class="ti ti-circle-key" style="color: var(--error);"></i>
</span>
<span :class="$style.title">{{ i18n.ts.youNeedToEnableTwoFactor }}</span>
</MkA>
<MkA <MkA
v-for="announcement in $i.unreadAnnouncements.filter(x => x.display === 'banner')" v-for="announcement in $i?.unreadAnnouncements.filter(x => x.display === 'banner')"
:key="announcement.id" :key="announcement.id"
:class="$style.item" :class="$style.item"
to="/announcements" to="/announcements"