From 7a6ac302f50031a7743ec89f8d660f0b28d3fceb Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sat, 21 Jun 2025 13:39:25 -0400 Subject: [PATCH] return assigned permissions from /i endpoint (resolves #657) --- .../src/core/entities/UserEntityService.ts | 30 +++++++++++++++++-- .../backend/src/models/json-schema/user.ts | 7 +++++ .../backend/src/server/api/ApiCallService.ts | 1 + packages/misskey-js/src/autogen/types.ts | 1 + 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index bc3672e6f5..47ea7c2282 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -5,6 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; +import * as Misskey from 'misskey-js'; import _Ajv from 'ajv'; import { ModuleRef } from '@nestjs/core'; import { In } from 'typeorm'; @@ -433,6 +434,7 @@ export class UserEntityService implements OnModuleInit { userMemos?: Map, pinNotes?: Map, iAmModerator?: boolean, + iAmAdmin?: boolean, userIdsByUri?: Map, instances?: Map, securityKeyCounts?: Map, @@ -480,6 +482,7 @@ export class UserEntityService implements OnModuleInit { const meId = me ? me.id : null; const isMe = meId === user.id; const iAmModerator = opts.iAmModerator ?? (me ? await this.roleService.isModerator(me as MiUser, opts.token) : false); + const iAmAdmin = opts.iAmAdmin ?? (me ? await this.roleService.isAdministrator(user, opts.token) : false); const profile = isDetailed ? (opts.userProfile ?? user.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) @@ -531,7 +534,6 @@ export class UserEntityService implements OnModuleInit { (profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount : null; - const isAdmin = isMe && isDetailed ? this.roleService.isAdministrator(user, opts.token) : null; const unreadAnnouncements = isMe && isDetailed ? (await this.announcementService.getUnreadAnnouncements(user)).map((announcement) => ({ createdAt: this.idService.parse(announcement.id).date.toISOString(), @@ -665,7 +667,7 @@ export class UserEntityService implements OnModuleInit { backgroundId: user.backgroundId, followedMessage: profile!.followedMessage, isModerator: iAmModerator, - isAdmin: isAdmin, + isAdmin: iAmAdmin, isSystem: isSystemAccount(user), injectFeaturedNote: profile!.injectFeaturedNote, receiveAnnouncementEmail: profile!.receiveAnnouncementEmail, @@ -700,6 +702,7 @@ export class UserEntityService implements OnModuleInit { achievements: profile!.achievements, loggedInDays: profile!.loggedInDates.length, policies: fetchPolicies(), + permissions: this.getPermissions(opts.token, iAmModerator, iAmAdmin), defaultCW: profile!.defaultCW, defaultCWPriority: profile!.defaultCWPriority, allowUnsignedFetch: user.allowUnsignedFetch, @@ -771,7 +774,11 @@ export class UserEntityService implements OnModuleInit { } const _userIds = _users.map(u => u.id); - const iAmModerator = await this.roleService.isModerator(me as MiUser, options?.token); + // Sync with ApiCallService + const roles = me ? await this.roleService.getUserRoles(me.id) : []; + const iAmAdmin = roles.some(r => r.isAdministrator) && (options?.token?.rank == null || options?.token.rank === 'admin'); + const iAmModerator = roles.some(r => r.isAdministrator || r.isModerator) && (options?.token?.rank == null || options?.token.rank === 'admin' || options?.token.rank === 'mod'); + const meId = me ? me.id : null; const isDetailed = options && options.schema !== 'UserLite'; const isDetailedAndMod = isDetailed && iAmModerator; @@ -868,6 +875,7 @@ export class UserEntityService implements OnModuleInit { userMemos: userMemos, pinNotes: pinNotes, iAmModerator, + iAmAdmin, userIdsByUri, instances, securityKeyCounts, @@ -876,4 +884,20 @@ export class UserEntityService implements OnModuleInit { )), ); } + + @bindThis + private getPermissions(token: MiAccessToken | null | undefined, isModerator: boolean, isAdmin: boolean): readonly string[] { + let permissions = token?.permission ?? Misskey.permissions; + + if (!isAdmin) { + permissions = permissions.filter(perm => !perm.startsWith('read:admin') && !perm.startsWith('write:admin')); + } + + // TODO support for moderator perms + // if (!isModerator) { + // + // } + + return permissions; + } } diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 65ef387fb7..69e81c7448 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -739,6 +739,13 @@ export const packedMeDetailedOnlySchema = { nullable: false, optional: false, ref: 'RolePolicies', }, + permissions: { + type: 'array', + nullable: false, optional: false, + items: { + type: 'string', + }, + }, twoFactorEnabled: { type: 'boolean', nullable: false, optional: false, diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index af00119f25..0db92031b3 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -379,6 +379,7 @@ export class ApiCallService implements OnApplicationShutdown { } if ((ep.meta.requireModerator || ep.meta.requireAdmin) && (this.meta.rootUserId !== user?.id)) { + // Sync with UserEntityService const myRoles = user ? await this.roleService.getUserRoles(user) : []; const isAdmin = myRoles.some(r => r.isAdministrator) && (token?.rank == null || token.rank === 'admin'); const isModerator = myRoles.some(r => r.isAdministrator || r.isModerator) && (token?.rank == null || token.rank === 'admin' || token.rank === 'mod'); diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index a96954ff11..0a562408e7 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4596,6 +4596,7 @@ export type components = { achievements: components['schemas']['Achievement'][]; loggedInDays: number; policies: components['schemas']['RolePolicies']; + permissions: string[]; /** @default false */ twoFactorEnabled: boolean; /** @default false */