This commit is contained in:
syuilo 2025-02-21 19:30:54 +09:00
parent 19d9fc56fa
commit e1523706a3
12 changed files with 76 additions and 39 deletions

View File

@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class SystemAccounts31740133121105 {
name = 'SystemAccounts31740133121105'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "rootUserId" character varying(32)`);
await queryRunner.query(`ALTER TABLE "meta" ADD CONSTRAINT "FK_c80e4079d632f95eac06a9d28cc" FOREIGN KEY ("rootUserId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
const users = await queryRunner.query(`SELECT "id" FROM "user" WHERE "isRoot" = true LIMIT 1`);
if (users.length > 0) {
await queryRunner.query(`UPDATE "meta" SET "rootUserId" = $1`, [users[0].id]);
}
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP CONSTRAINT "FK_c80e4079d632f95eac06a9d28cc"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "rootUserId"`);
}
}

View File

@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Not, IsNull } from 'typeorm';
import type { FollowingsRepository, MiUser, UsersRepository } from '@/models/_.js';
import type { FollowingsRepository, MiMeta, MiUser, UsersRepository } from '@/models/_.js';
import { QueueService } from '@/core/QueueService.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
@ -18,6 +18,9 @@ import { SystemAccountService } from '@/core/SystemAccountService.js';
@Injectable()
export class DeleteAccountService {
constructor(
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@ -38,8 +41,9 @@ export class DeleteAccountService {
id: string;
host: string | null;
}, moderator?: MiUser): Promise<void> {
if (this.meta.rootUserId === user.id) throw new Error('cannot delete a root account');
const _user = await this.usersRepository.findOneByOrFail({ id: user.id });
if (_user.isRoot) throw new Error('cannot delete a root account');
const systemAccounts = await this.systemAccountService.list();
for (const systemAccount of systemAccounts) {

View File

@ -406,15 +406,15 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
}
@bindThis
public async isModerator(user: { id: MiUser['id']; isRoot: MiUser['isRoot'] } | null): Promise<boolean> {
public async isModerator(user: { id: MiUser['id'] } | null): Promise<boolean> {
if (user == null) return false;
return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isModerator || r.isAdministrator);
return (this.meta.rootUserId === user.id) || (await this.getUserRoles(user.id)).some(r => r.isModerator || r.isAdministrator);
}
@bindThis
public async isAdministrator(user: { id: MiUser['id']; isRoot: MiUser['isRoot'] } | null): Promise<boolean> {
public async isAdministrator(user: { id: MiUser['id'] } | null): Promise<boolean> {
if (user == null) return false;
return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isAdministrator);
return (this.meta.rootUserId === user.id) || (await this.getUserRoles(user.id)).some(r => r.isAdministrator);
}
@bindThis
@ -463,16 +463,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
.map(a => a.userId),
);
if (includeRoot) {
const rootUserId = await this.rootUserIdCache.fetch(async () => {
const it = await this.usersRepository.createQueryBuilder('users')
.select('id')
.where({ isRoot: true })
.getRawOne<{ id: string }>();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return it!.id;
});
resultSet.add(rootUserId);
if (includeRoot && this.meta.rootUserId) {
resultSet.add(this.meta.rootUserId);
}
return [...resultSet].sort((x, y) => x.localeCompare(y));

View File

@ -21,6 +21,7 @@ import UsersChart from '@/core/chart/charts/users.js';
import { UtilityService } from '@/core/UtilityService.js';
import { UserService } from '@/core/UserService.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
import { MetaService } from '@/core/MetaService.js';
@Injectable()
export class SignupService {
@ -42,6 +43,7 @@ export class SignupService {
private userEntityService: UserEntityService,
private idService: IdService,
private systemAccountService: SystemAccountService,
private metaService: MetaService,
private usersChart: UsersChart,
) {
}
@ -86,9 +88,7 @@ export class SignupService {
throw new Error('USED_USERNAME');
}
const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent(); // TODO
if (!opts.ignorePreservedUsernames && !isTheFirstUser) {
if (!opts.ignorePreservedUsernames && this.meta.rootUserId != null) {
const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
if (isPreserved) {
throw new Error('USED_USERNAME');
@ -129,7 +129,6 @@ export class SignupService {
usernameLower: username.toLowerCase(),
host: this.utilityService.toPunyNullable(host),
token: secret,
isRoot: isTheFirstUser,
}));
await transactionalEntityManager.save(new MiUserKeypair({
@ -153,6 +152,10 @@ export class SignupService {
this.usersChart.update(account, true);
this.userService.notifySystemWebhook(account, 'userCreated');
if (this.meta.rootUserId == null) {
await this.metaService.update({ rootUserId: account.id });
}
return { account, secret };
}
}

View File

@ -103,7 +103,6 @@ export class SystemAccountService {
usernameLower: extra.username.toLowerCase(),
host: null,
token: secret,
isRoot: false,
isLocked: true,
isExplorable: false,
isBot: true,

View File

@ -152,7 +152,7 @@ export class MetaEntityService {
...packed,
cacheRemoteFiles: instance.cacheRemoteFiles,
cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles,
requireSetup: !await this.instanceActorService.realLocalUsersPresent(), // TODO
requireSetup: this.meta.rootUserId == null,
proxyAccountName: proxyAccount.username,
features: {
localTimeline: instance.policies.ltlAvailable,

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
import { Entity, Column, PrimaryColumn, ManyToOne } from 'typeorm';
import { id } from './util/id.js';
import { MiUser } from './User.js';
@ -15,6 +15,18 @@ export class MiMeta {
})
public id: string;
@Column({
...id(),
nullable: true,
})
public rootUserId: MiUser['id'] | null;
@ManyToOne(type => MiUser, {
onDelete: 'SET NULL',
nullable: true,
})
public rootUser: MiUser | null;
@Column('varchar', {
length: 1024, nullable: true,
})

View File

@ -184,12 +184,6 @@ export class MiUser {
})
public isCat: boolean;
@Column('boolean', {
default: false,
comment: 'Whether the User is the root.',
})
public isRoot: boolean;
@Index()
@Column('boolean', {
default: true,

View File

@ -371,7 +371,7 @@ export class ApiCallService implements OnApplicationShutdown {
}
}
if ((ep.meta.requireModerator || ep.meta.requireAdmin) && !user!.isRoot) {
if ((ep.meta.requireModerator || ep.meta.requireAdmin) && (this.meta.rootUserId !== user!.id)) {
const myRoles = await this.roleService.getUserRoles(user!.id);
if (ep.meta.requireModerator && !myRoles.some(r => r.isModerator || r.isAdministrator)) {
throw new ApiError({
@ -391,7 +391,7 @@ export class ApiCallService implements OnApplicationShutdown {
}
}
if (ep.meta.requireRolePolicy != null && !user!.isRoot) {
if (ep.meta.requireRolePolicy != null && (this.meta.rootUserId !== user!.id)) {
const myRoles = await this.roleService.getUserRoles(user!.id);
const policies = await this.roleService.getUserPolicies(user!.id);
if (!policies[ep.meta.requireRolePolicy] && !myRoles.some(r => r.isAdministrator)) {

View File

@ -6,7 +6,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js';
import type { MiMeta, UsersRepository } from '@/models/_.js';
import { SignupService } from '@/core/SignupService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { localUsernameSchema, passwordSchema } from '@/models/User.js';
@ -61,6 +61,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.config)
private config: Config,
@Inject(DI.meta)
private serverSettings: MiMeta,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@ -69,9 +72,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, async (ps, _me, token) => {
const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null;
const realUsers = await this.instanceActorService.realLocalUsersPresent(); // TODO
if (!realUsers && me == null && token == null) {
if (this.serverSettings.rootUserId == null && me == null && token == null) {
// 初回セットアップの場合
if (this.config.setupPassword != null) {
// 初期パスワードが設定されている場合
@ -83,7 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// 初期パスワードが設定されていないのに初期パスワードが入力された場合
throw new ApiError(meta.errors.wrongInitialPassword);
}
} else if ((realUsers && !me?.isRoot) || token !== null) {
} else if ((this.serverSettings.rootUserId != null && (this.serverSettings.rootUserId !== me?.id)) || token !== null) {
// 初回セットアップではなく、管理者でない場合 or 外部トークンを使用している場合
throw new ApiError(meta.errors.accessDenied);
}

View File

@ -6,7 +6,7 @@
import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
import type { UsersRepository, UserProfilesRepository, MiMeta } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
@ -43,6 +43,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@ -58,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error('user not found');
}
if (user.isRoot) {
if (this.serverSettings.rootUserId === user.id) {
throw new Error('cannot reset password of root');
}

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
@ -19,6 +19,8 @@ import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import * as Acct from '@/misc/acct.js';
import { DI } from '@/di-symbols.js';
import { MiMeta } from '@/models/_.js';
export const meta = {
tags: ['users'],
@ -81,6 +83,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
private remoteUserResolveService: RemoteUserResolveService,
private apiLoggerService: ApiLoggerService,
private accountMoveService: AccountMoveService,
@ -92,7 +97,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// check parameter
if (!ps.moveToAccount) throw new ApiError(meta.errors.noSuchUser);
// abort if user is the root
if (me.isRoot) throw new ApiError(meta.errors.rootForbidden);
if (this.serverSettings.rootUserId === me.id) throw new ApiError(meta.errors.rootForbidden);
// abort if user has already moved
if (me.movedToUri) throw new ApiError(meta.errors.alreadyMoved);