diff options
| author | Hazel K <acomputerdog@gmail.com> | 2024-10-07 10:02:49 -0400 |
|---|---|---|
| committer | Hazel K <acomputerdog@gmail.com> | 2024-10-07 10:02:55 -0400 |
| commit | a790fef2613015fac0bc4fb119abaa1f48de5841 (patch) | |
| tree | 8245eedd5d243423de68c380be74a7cd3593c4b4 | |
| parent | merge: Add controls to delete all files or sever all relations with a remote ... (diff) | |
| download | sharkey-a790fef2613015fac0bc4fb119abaa1f48de5841.tar.gz sharkey-a790fef2613015fac0bc4fb119abaa1f48de5841.tar.bz2 sharkey-a790fef2613015fac0bc4fb119abaa1f48de5841.zip | |
prevent deletion or suspension of system accounts
9 files changed, 72 insertions, 3 deletions
diff --git a/packages/backend/src/core/DeleteAccountService.ts b/packages/backend/src/core/DeleteAccountService.ts index 7f1b8f3efb..8408e95863 100644 --- a/packages/backend/src/core/DeleteAccountService.ts +++ b/packages/backend/src/core/DeleteAccountService.ts @@ -13,6 +13,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { isSystemAccount } from '@/misc/is-system-account.js'; @Injectable() export class DeleteAccountService { @@ -38,6 +39,7 @@ export class DeleteAccountService { }, moderator?: MiUser): Promise<void> { const _user = await this.usersRepository.findOneByOrFail({ id: user.id }); if (_user.isRoot) throw new Error('cannot delete a root account'); + if (isSystemAccount(_user)) throw new Error('cannot delete a system account'); if (moderator != null) { this.moderationLogService.log(moderator, 'deleteAccount', { diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts index 7920e58e36..30dcaa6f7d 100644 --- a/packages/backend/src/core/UserSuspendService.ts +++ b/packages/backend/src/core/UserSuspendService.ts @@ -15,6 +15,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import { RelationshipJobData } from '@/queue/types.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { isSystemAccount } from '@/misc/is-system-account.js'; @Injectable() export class UserSuspendService { @@ -38,6 +39,8 @@ export class UserSuspendService { @bindThis public async suspend(user: MiUser, moderator: MiUser): Promise<void> { + if (isSystemAccount(user)) throw new Error('cannot suspend a system account'); + await this.usersRepository.update(user.id, { isSuspended: true, }); diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 40830f86b4..d465e2cd4c 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -53,6 +53,7 @@ import type { OnModuleInit } from '@nestjs/common'; import type { NoteEntityService } from './NoteEntityService.js'; import type { DriveFileEntityService } from './DriveFileEntityService.js'; import type { PageEntityService } from './PageEntityService.js'; +import { isSystemAccount } from '@/misc/is-system-account.js'; const Ajv = _Ajv.default; const ajv = new Ajv(); @@ -614,6 +615,7 @@ export class UserEntityService implements OnModuleInit { backgroundId: user.backgroundId, isModerator: isModerator, isAdmin: isAdmin, + isSystem: isSystemAccount(user), injectFeaturedNote: profile!.injectFeaturedNote, receiveAnnouncementEmail: profile!.receiveAnnouncementEmail, alwaysMarkNsfw: profile!.alwaysMarkNsfw, diff --git a/packages/backend/src/misc/is-system-account.ts b/packages/backend/src/misc/is-system-account.ts new file mode 100644 index 0000000000..0b699944b3 --- /dev/null +++ b/packages/backend/src/misc/is-system-account.ts @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/** + * Checks if the given user represents a system account, such as instance.actor. + */ +export function isSystemAccount(user: { readonly username: string }): boolean { + return user.username.includes('.'); +} diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 249b9bba38..24b6c50e93 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -121,6 +121,11 @@ export const packedUserLiteSchema = { nullable: false, optional: true, default: false, }, + isSystem: { + type: 'boolean', + nullable: false, optional: true, + default: false, + }, isSilenced: { type: 'boolean', nullable: false, optional: false, diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index a7ca7f9547..dda6a0e882 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -11,6 +11,7 @@ import { RoleService } from '@/core/RoleService.js'; import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; import { IdService } from '@/core/IdService.js'; import { notificationRecieveConfig } from '@/models/json-schema/user.js'; +import { isSystemAccount } from '@/misc/is-system-account.js'; export const meta = { tags: ['admin'], @@ -111,6 +112,10 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + isSystem: { + type: 'boolean', + optional: false, nullable: false, + }, isSilenced: { type: 'boolean', optional: false, nullable: false, @@ -240,6 +245,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- mutedInstances: profile.mutedInstances, notificationRecieveConfig: profile.notificationRecieveConfig, isModerator: isModerator, + isSystem: isSystemAccount(user), isSilenced: isSilenced, isSuspended: user.isSuspended, isHibernated: user.isHibernated, diff --git a/packages/backend/test/unit/misc/is-system-account.ts b/packages/backend/test/unit/misc/is-system-account.ts new file mode 100644 index 0000000000..045fe04477 --- /dev/null +++ b/packages/backend/test/unit/misc/is-system-account.ts @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { isSystemAccount } from '@/misc/is-system-account.js'; + +describe(isSystemAccount, () => { + it('should return true for instance.actor', () => { + expect(isSystemAccount({ username: 'instance.actor' })).toBeTruthy(); + }); + + it('should return true for relay.actor', () => { + expect(isSystemAccount({ username: 'relay.actor' })).toBeTruthy(); + }); + + it('should return true for any username with a dot', () => { + expect(isSystemAccount({ username: 'some.user' })).toBeTruthy(); + expect(isSystemAccount({ username: 'some.' })).toBeTruthy(); + expect(isSystemAccount({ username: '.user' })).toBeTruthy(); + expect(isSystemAccount({ username: '.' })).toBeTruthy(); + }); + + it('should return true for usernames with multiple dots', () => { + expect(isSystemAccount({ username: 'some.user.account' })).toBeTruthy(); + expect(isSystemAccount({ username: '..' })).toBeTruthy(); + }); + + it('should return false for usernames without a dot', () => { + expect(isSystemAccount({ username: 'instance_actor' })).toBeFalsy(); + expect(isSystemAccount({ username: 'instanceactor' })).toBeFalsy(); + expect(isSystemAccount({ username: 'relay_actor' })).toBeFalsy(); + expect(isSystemAccount({ username: 'relayactor' })).toBeFalsy(); + expect(isSystemAccount({ username: '' })).toBeFalsy(); + }); +}); diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index 187ec66b42..70fd2eb927 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> - <MkInfo v-if="['instance.actor', 'relay.actor'].includes(user.username)">{{ i18n.ts.isSystemAccount }}</MkInfo> + <MkInfo v-if="isSystem">{{ i18n.ts.isSystemAccount }}</MkInfo> <FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ i18n.ts.instanceInfo }}</FormLink> @@ -79,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSection> <div class="_gaps"> <MkSwitch v-model="silenced" @update:modelValue="toggleSilence">{{ i18n.ts.silence }}</MkSwitch> - <MkSwitch v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch> + <MkSwitch v-if="!isSystem" v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch> <MkSwitch v-model="markedAsNSFW" @update:modelValue="toggleNSFW">{{ i18n.ts.markAsNSFW }}</MkSwitch> <div> @@ -114,7 +114,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="unsetUserBanner"><i class="ti ti-photo"></i> {{ i18n.ts.unsetUserBanner }}</MkButton> <MkButton v-if="iAmModerator" inline danger @click="deleteAllFiles"><i class="ph-cloud ph-bold ph-lg"></i> {{ i18n.ts.deleteAllFiles }}</MkButton> </div> - <MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton> + <MkButton v-if="$i.isAdmin && !isSystem" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton> </div> </FormSection> </div> @@ -236,6 +236,7 @@ const approved = ref(false); const suspended = ref(false); const markedAsNSFW = ref(false); const moderationNote = ref(''); +const isSystem = computed(() => user.value?.isSystem ?? false); const filesPagination = { endpoint: 'admin/drive/files' as const, limit: 10, diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 79b4e4a038..adffa36950 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -3820,6 +3820,8 @@ export type components = { isAdmin?: boolean; /** @default false */ isModerator?: boolean; + /** @default false */ + isSystem?: boolean; isSilenced: boolean; noindex: boolean; isBot?: boolean; @@ -9199,6 +9201,7 @@ export type operations = { }]>; }; isModerator: boolean; + isSystem: boolean; isSilenced: boolean; isSuspended: boolean; isHibernated: boolean; |