summaryrefslogtreecommitdiff
path: root/packages/backend/src
diff options
context:
space:
mode:
authorDaiki Mizukami <tesaguriguma@gmail.com>2024-08-09 12:10:51 +0900
committerGitHub <noreply@github.com>2024-08-09 12:10:51 +0900
commit0d508db8a7a36218d38231af4e718aff0e94d9bc (patch)
treee17b48907ba006f0492e77084093015516ce4b42 /packages/backend/src
parentci: change prerelease channels to alpha, beta, and rc (#14376) (diff)
downloadsharkey-0d508db8a7a36218d38231af4e718aff0e94d9bc.tar.gz
sharkey-0d508db8a7a36218d38231af4e718aff0e94d9bc.tar.bz2
sharkey-0d508db8a7a36218d38231af4e718aff0e94d9bc.zip
fix(backend): check visibility of following/followers of remote users / feat: moderators can see following/followers of all users (#14375)
* fix(backend): check visibility of following/followers of remote users Resolves https://github.com/misskey-dev/misskey/issues/13362. * test(backend): add tests for visibility of following/followers of remote users * docs(changelog): update CHANGELOG.md * feat: moderators can see following/followers of all users * docs(changelog): update CHANGELOG.md * refactor(backend): minor refactoring `createPerson`と`if`の条件を統一するとともに、異常系の 処理をearly returnに追い出すための変更。 * feat(backend): moderators can see following/followers count of all users As per https://github.com/misskey-dev/misskey/pull/14375#issuecomment-2275044908.
Diffstat (limited to 'packages/backend/src')
-rw-r--r--packages/backend/src/core/activitypub/models/ApPersonService.ts50
-rw-r--r--packages/backend/src/core/activitypub/type.ts6
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/users/followers.ts34
-rw-r--r--packages/backend/src/server/api/endpoints/users/following.ts34
5 files changed, 93 insertions, 35 deletions
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index 457205e023..f3ddf3952c 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -48,7 +48,7 @@ import type { ApResolverService, Resolver } from '../ApResolverService.js';
import type { ApLoggerService } from '../ApLoggerService.js';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import type { ApImageService } from './ApImageService.js';
-import type { IActor, IObject } from '../type.js';
+import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js';
const nameLength = 128;
const summaryLength = 2048;
@@ -296,6 +296,21 @@ export class ApPersonService implements OnModuleInit {
const isBot = getApType(object) === 'Service' || getApType(object) === 'Application';
+ const [followingVisibility, followersVisibility] = await Promise.all(
+ [
+ this.isPublicCollection(person.following, resolver),
+ this.isPublicCollection(person.followers, resolver),
+ ].map((p): Promise<'public' | 'private'> => p
+ .then(isPublic => isPublic ? 'public' : 'private')
+ .catch(err => {
+ if (!(err instanceof StatusError) || err.isRetryable) {
+ this.logger.error('error occurred while fetching following/followers collection', { stack: err });
+ }
+ return 'private';
+ })
+ )
+ );
+
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
const url = getOneApHrefNullable(person.url);
@@ -357,6 +372,8 @@ export class ApPersonService implements OnModuleInit {
description: _description,
url,
fields,
+ followingVisibility,
+ followersVisibility,
birthday: bday?.[0] ?? null,
location: person['vcard:Address'] ?? null,
userHost: host,
@@ -464,6 +481,23 @@ export class ApPersonService implements OnModuleInit {
const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32);
+ const [followingVisibility, followersVisibility] = await Promise.all(
+ [
+ this.isPublicCollection(person.following, resolver),
+ this.isPublicCollection(person.followers, resolver),
+ ].map((p): Promise<'public' | 'private' | undefined> => p
+ .then(isPublic => isPublic ? 'public' : 'private')
+ .catch(err => {
+ if (!(err instanceof StatusError) || err.isRetryable) {
+ this.logger.error('error occurred while fetching following/followers collection', { stack: err });
+ // Do not update the visibiility on transient errors.
+ return undefined;
+ }
+ return 'private';
+ })
+ )
+ );
+
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
const url = getOneApHrefNullable(person.url);
@@ -532,6 +566,8 @@ export class ApPersonService implements OnModuleInit {
url,
fields,
description: _description,
+ followingVisibility,
+ followersVisibility,
birthday: bday?.[0] ?? null,
location: person['vcard:Address'] ?? null,
});
@@ -703,4 +739,16 @@ export class ApPersonService implements OnModuleInit {
return 'ok';
}
+
+ @bindThis
+ private async isPublicCollection(collection: string | ICollection | IOrderedCollection | undefined, resolver: Resolver): Promise<boolean> {
+ if (collection) {
+ const resolved = await resolver.resolveCollection(collection);
+ if (resolved.first || (resolved as ICollection).items || (resolved as IOrderedCollection).orderedItems) {
+ return true;
+ }
+ }
+
+ return false;
+ }
}
diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts
index 5b6c6c8ca6..131c518c0a 100644
--- a/packages/backend/src/core/activitypub/type.ts
+++ b/packages/backend/src/core/activitypub/type.ts
@@ -97,13 +97,15 @@ export interface IActivity extends IObject {
export interface ICollection extends IObject {
type: 'Collection';
totalItems: number;
- items: ApObject;
+ first?: IObject | string;
+ items?: ApObject;
}
export interface IOrderedCollection extends IObject {
type: 'OrderedCollection';
totalItems: number;
- orderedItems: ApObject;
+ first?: IObject | string;
+ orderedItems?: ApObject;
}
export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video', 'Event'];
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index 7fd093c191..9bf568bc90 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -454,12 +454,12 @@ export class UserEntityService implements OnModuleInit {
}
const followingCount = profile == null ? null :
- (profile.followingVisibility === 'public') || isMe ? user.followingCount :
+ (profile.followingVisibility === 'public') || isMe || iAmModerator ? user.followingCount :
(profile.followingVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount :
null;
const followersCount = profile == null ? null :
- (profile.followersVisibility === 'public') || isMe ? user.followersCount :
+ (profile.followersVisibility === 'public') || isMe || iAmModerator ? user.followersCount :
(profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
null;
diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts
index 7ce7734f53..a8b4319a61 100644
--- a/packages/backend/src/server/api/endpoints/users/followers.ts
+++ b/packages/backend/src/server/api/endpoints/users/followers.ts
@@ -11,6 +11,7 @@ import { QueryService } from '@/core/QueryService.js';
import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { DI } from '@/di-symbols.js';
+import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -81,6 +82,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private utilityService: UtilityService,
private followingEntityService: FollowingEntityService,
private queryService: QueryService,
+ private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy(ps.userId != null
@@ -93,22 +95,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
- if (profile.followersVisibility === 'private') {
- if (me == null || (me.id !== user.id)) {
- throw new ApiError(meta.errors.forbidden);
- }
- } else if (profile.followersVisibility === 'followers') {
- if (me == null) {
- throw new ApiError(meta.errors.forbidden);
- } else if (me.id !== user.id) {
- const isFollowing = await this.followingsRepository.exists({
- where: {
- followeeId: user.id,
- followerId: me.id,
- },
- });
- if (!isFollowing) {
+ if (profile.followersVisibility !== 'public' && !await this.roleService.isModerator(me)) {
+ if (profile.followersVisibility === 'private') {
+ if (me == null || (me.id !== user.id)) {
+ throw new ApiError(meta.errors.forbidden);
+ }
+ } else if (profile.followersVisibility === 'followers') {
+ if (me == null) {
throw new ApiError(meta.errors.forbidden);
+ } else if (me.id !== user.id) {
+ const isFollowing = await this.followingsRepository.exists({
+ where: {
+ followeeId: user.id,
+ followerId: me.id,
+ },
+ });
+ if (!isFollowing) {
+ throw new ApiError(meta.errors.forbidden);
+ }
}
}
}
diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts
index 6b3389f0b2..feda5bb353 100644
--- a/packages/backend/src/server/api/endpoints/users/following.ts
+++ b/packages/backend/src/server/api/endpoints/users/following.ts
@@ -12,6 +12,7 @@ import { QueryService } from '@/core/QueryService.js';
import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { DI } from '@/di-symbols.js';
+import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -90,6 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private utilityService: UtilityService,
private followingEntityService: FollowingEntityService,
private queryService: QueryService,
+ private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy(ps.userId != null
@@ -102,22 +104,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
- if (profile.followingVisibility === 'private') {
- if (me == null || (me.id !== user.id)) {
- throw new ApiError(meta.errors.forbidden);
- }
- } else if (profile.followingVisibility === 'followers') {
- if (me == null) {
- throw new ApiError(meta.errors.forbidden);
- } else if (me.id !== user.id) {
- const isFollowing = await this.followingsRepository.exists({
- where: {
- followeeId: user.id,
- followerId: me.id,
- },
- });
- if (!isFollowing) {
+ if (profile.followingVisibility !== 'public' && !await this.roleService.isModerator(me)) {
+ if (profile.followingVisibility === 'private') {
+ if (me == null || (me.id !== user.id)) {
+ throw new ApiError(meta.errors.forbidden);
+ }
+ } else if (profile.followingVisibility === 'followers') {
+ if (me == null) {
throw new ApiError(meta.errors.forbidden);
+ } else if (me.id !== user.id) {
+ const isFollowing = await this.followingsRepository.exists({
+ where: {
+ followeeId: user.id,
+ followerId: me.id,
+ },
+ });
+ if (!isFollowing) {
+ throw new ApiError(meta.errors.forbidden);
+ }
}
}
}