pass client token through API via caller ID

This commit is contained in:
Hazelnoot 2025-06-22 15:34:52 -04:00
parent 2e61fafe57
commit df750a6b65
5 changed files with 85 additions and 13 deletions

View file

@ -342,11 +342,11 @@ export class ChatService {
}
@bindThis
public async hasPermissionToViewRoomTimeline(meId: MiUser['id'], room: MiChatRoom) {
if (await this.isRoomMember(room, meId)) {
public async hasPermissionToViewRoomTimeline(me: MiUser, room: MiChatRoom) {
if (await this.isRoomMember(room, me.id)) {
return true;
} else {
const iAmModerator = await this.roleService.isModerator({ id: meId });
const iAmModerator = await this.roleService.isModerator(me);
if (iAmModerator) {
return true;
}
@ -563,12 +563,12 @@ export class ChatService {
}
@bindThis
public async hasPermissionToDeleteRoom(meId: MiUser['id'], room: MiChatRoom) {
if (room.ownerId === meId) {
public async hasPermissionToDeleteRoom(me: MiUser, room: MiChatRoom) {
if (room.ownerId === me.id) {
return true;
}
const iAmModerator = await this.roleService.isModerator({ id: meId });
const iAmModerator = await this.roleService.isModerator(me);
if (iAmModerator) {
return true;
}

View file

@ -31,6 +31,7 @@ import type { Packed } from '@/misc/json-schema.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { NotificationService } from '@/core/NotificationService.js';
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
import { getCallerId } from '@/misc/attach-caller-id.js';
export type RolePolicies = {
gtlAvailable: boolean;
@ -414,7 +415,21 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id));
const user = typeof(userOrId) === 'object' ? userOrId : roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userOrId) : null;
const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, assignedRoles, r.condFormula, followStats));
return [...assignedRoles, ...matchedCondRoles];
let allRoles = [...assignedRoles, ...matchedCondRoles];
// Check for dropped token permissions
const rank = user ? getCallerId(user)?.accessToken?.rank : null;
if (rank != null) {
// Copy roles, since they come from a cache
allRoles = allRoles.map(role => ({
...role,
isModerator: role.isModerator && (rank === 'admin' || rank === 'mod'),
isAdministrator: role.isAdministrator && rank === 'admin',
}));
}
return allRoles;
}
/**
@ -514,12 +529,22 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
@bindThis
public async isModerator(user: { id: MiUser['id'] } | null): Promise<boolean> {
if (user == null) return false;
// Check for dropped token permissions
const callerId = getCallerId(user);
if (callerId?.accessToken?.rank != null && callerId.accessToken.rank !== 'admin' && callerId.accessToken.rank !== 'mod') return false;
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'] } | null): Promise<boolean> {
if (user == null) return false;
// Check for dropped token permissions
const callerId = getCallerId(user);
if (callerId?.accessToken?.rank != null && callerId.accessToken.rank !== 'admin') return false;
return (this.meta.rootUserId === user.id) || (await this.getUserRoles(user.id)).some(r => r.isAdministrator);
}

View file

@ -481,8 +481,8 @@ export class UserEntityService implements OnModuleInit {
const isDetailed = opts.schema !== 'UserLite';
const meId = me ? me.id : null;
const isMe = meId === user.id;
const iAmModerator = opts.iAmModerator ?? (me ? await this.roleService.isModerator(me as MiUser) : false);
const iAmAdmin = opts.iAmAdmin ?? (me ? await this.roleService.isAdministrator(user) : false);
const iAmModerator = opts.iAmModerator ?? (me ? await this.roleService.isModerator(me) : false);
const iAmAdmin = opts.iAmAdmin ?? (me ? await this.roleService.isAdministrator(me) : false);
const profile = isDetailed
? (opts.userProfile ?? user.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }))

View file

@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { MiAccessToken } from '@/models/AccessToken.js';
const callerIdSymbol = Symbol('callerId');
/**
* Client metadata associated with an object (typically an instance of MiUser).
*/
export interface CallerId {
/**
* Client's access token, or null if no token was used.
*/
accessToken?: MiAccessToken | null;
}
interface ObjectWithCallerId {
[callerIdSymbol]?: CallerId;
}
/**
* Attaches client metadata to an object.
* Calling this repeatedly will overwrite the previous value.
* Pass undefined to erase the attached data.
* @param target Object to attach to (typically an instance of MiUser).
* @param callerId Data to attach.
*/
export function attachCallerId(target: object, callerId: CallerId | undefined): void {
(target as ObjectWithCallerId)[callerIdSymbol] = callerId;
}
/**
* Fetches client metadata from an object.
* Returns undefined if no metadata is attached.
* @param target Object to fetch from.
*/
export function getCallerId(target: object): CallerId | undefined {
return (target as ObjectWithCallerId)[callerIdSymbol];
}

View file

@ -13,6 +13,7 @@ import type { MiApp } from '@/models/App.js';
import { CacheService } from '@/core/CacheService.js';
import { isNativeUserToken } from '@/misc/token.js';
import { bindThis } from '@/decorators.js';
import { attachCallerId } from '@/misc/attach-caller-id.js';
export class AuthenticationError extends Error {
constructor(message: string) {
@ -62,6 +63,9 @@ export class AuthenticateService implements OnApplicationShutdown {
}, {
token: token, // miauth
}],
relations: {
user: true,
},
});
if (accessToken == null) {
@ -72,10 +76,11 @@ export class AuthenticateService implements OnApplicationShutdown {
lastUsedAt: new Date(),
});
const user = await this.cacheService.localUserByIdCache.fetch(accessToken.userId,
() => this.usersRepository.findOneBy({
id: accessToken.userId,
}) as Promise<MiLocalUser>);
// Loaded by relation above
const user = accessToken.user as MiLocalUser;
// Attach token to user - this will be read by RoleService to drop admin/moderator permissions.
attachCallerId(user, { accessToken });
if (accessToken.appId) {
const app = await this.appCache.fetch(accessToken.appId,