merge: always validate role policies (!1176)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1176 Approved-by: Hazelnoot <acomputerdog@gmail.com> Approved-by: Marie <github@yuugi.dev>
This commit is contained in:
commit
cfbffefb7f
4 changed files with 209 additions and 19 deletions
|
|
@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import * as Redis from 'ioredis';
|
||||
import { In } from 'typeorm';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import Ajv from 'ajv';
|
||||
import type {
|
||||
MiMeta,
|
||||
MiRole,
|
||||
|
|
@ -35,8 +36,10 @@ import {
|
|||
type ManagedMemorySingleCache,
|
||||
type ManagedMemoryKVCache,
|
||||
} from '@/global/CacheManagementService.js';
|
||||
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { getCallerId } from '@/misc/attach-caller-id.js';
|
||||
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
|
||||
import type { JSONSchemaType, ValidateFunction } from 'ajv';
|
||||
|
||||
export type RolePolicies = {
|
||||
gtlAvailable: boolean;
|
||||
|
|
@ -122,6 +125,86 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||
|
||||
// TODO cache sync fixes (and maybe events too?)
|
||||
|
||||
const DefaultPoliciesSchema: JSONSchemaType<RolePolicies> = {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: [],
|
||||
properties: {
|
||||
gtlAvailable: { type: 'boolean' },
|
||||
ltlAvailable: { type: 'boolean' },
|
||||
btlAvailable: { type: 'boolean' },
|
||||
canPublicNote: { type: 'boolean' },
|
||||
scheduleNoteMax: { type: 'integer', minimum: 0 },
|
||||
mentionLimit: { type: 'integer', minimum: 0 },
|
||||
canInvite: { type: 'boolean' },
|
||||
inviteLimit: { type: 'integer', minimum: 0 },
|
||||
inviteLimitCycle: { type: 'integer', minimum: 0 },
|
||||
inviteExpirationTime: { type: 'integer', minimum: 0 },
|
||||
canManageCustomEmojis: { type: 'boolean' },
|
||||
canManageAvatarDecorations: { type: 'boolean' },
|
||||
canSearchNotes: { type: 'boolean' },
|
||||
canUseTranslator: { type: 'boolean' },
|
||||
canHideAds: { type: 'boolean' },
|
||||
|
||||
// these can be less than 1 MB
|
||||
// (test/unit/server/api/drive/files/create.ts depends on this)
|
||||
driveCapacityMb: { type: 'number', minimum: 0 },
|
||||
maxFileSizeMb: { type: 'number', minimum: 0 },
|
||||
|
||||
alwaysMarkNsfw: { type: 'boolean' },
|
||||
canUpdateBioMedia: { type: 'boolean' },
|
||||
pinLimit: { type: 'integer', minimum: 0 },
|
||||
antennaLimit: { type: 'integer', minimum: 0 },
|
||||
wordMuteLimit: { type: 'integer', minimum: 0 },
|
||||
webhookLimit: { type: 'integer', minimum: 0 },
|
||||
clipLimit: { type: 'integer', minimum: 0 },
|
||||
noteEachClipsLimit: { type: 'integer', minimum: 0 },
|
||||
userListLimit: { type: 'integer', minimum: 0 },
|
||||
userEachUserListsLimit: { type: 'integer', minimum: 0 },
|
||||
rateLimitFactor: { type: 'number', minimum: 0.01 },
|
||||
canImportNotes: { type: 'boolean' },
|
||||
avatarDecorationLimit: { type: 'integer', minimum: 0 },
|
||||
canImportAntennas: { type: 'boolean' },
|
||||
canImportBlocking: { type: 'boolean' },
|
||||
canImportFollowing: { type: 'boolean' },
|
||||
canImportMuting: { type: 'boolean' },
|
||||
canImportUserLists: { type: 'boolean' },
|
||||
chatAvailability: { type: 'string', enum: ['available', 'readonly', 'unavailable'] },
|
||||
canTrend: { type: 'boolean' },
|
||||
canViewFederation: { type: 'boolean' },
|
||||
},
|
||||
};
|
||||
|
||||
const RoleSchema: JSONSchemaType<MiRole['policies']> = {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: [],
|
||||
properties: Object.fromEntries(
|
||||
Object.entries(DefaultPoliciesSchema.properties!).map(
|
||||
(
|
||||
// I picked `canTrend` here, but any policy name is fine, the
|
||||
// type of their bit of the schema is all the same
|
||||
[policy, value]: [string, JSONSchemaType<RolePolicies>['properties']['canTrend']]
|
||||
) => [
|
||||
policy,
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
// we can't require `value` because the MiRole says `value:
|
||||
// any` which includes undefined, so technically `value` is
|
||||
// not really required
|
||||
required: ['priority', 'useDefault'],
|
||||
properties: {
|
||||
priority: { type: 'integer', minimum: 0, maximum: 2 },
|
||||
useDefault: { type: 'boolean' },
|
||||
value,
|
||||
},
|
||||
},
|
||||
],
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
private readonly rolesCache: ManagedMemorySingleCache<MiRole[]>;
|
||||
|
|
@ -129,6 +212,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
|
||||
private cacheService: CacheService;
|
||||
private notificationService: NotificationService;
|
||||
private defaultPoliciesValidator: ValidateFunction<RolePolicies>;
|
||||
private roleValidator: ValidateFunction<MiRole['policies']>;
|
||||
|
||||
public static AlreadyAssignedError = class extends Error {};
|
||||
public static NotAssignedError = class extends Error {};
|
||||
|
|
@ -167,6 +252,14 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
// TODO additional cache for final calculation?
|
||||
|
||||
this.redisForSub.on('message', this.onMessage);
|
||||
|
||||
// this is copied from server/api/endpoint-base.ts
|
||||
const ajv = new Ajv.default({
|
||||
useDefaults: true,
|
||||
allErrors: true,
|
||||
});
|
||||
this.defaultPoliciesValidator = ajv.compile(DefaultPoliciesSchema);
|
||||
this.roleValidator = ajv.compile(RoleSchema);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
@ -756,6 +849,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
|
||||
@bindThis
|
||||
public async create(values: Partial<MiRole>, moderator?: MiUser): Promise<MiRole> {
|
||||
this.assertValidRole(values);
|
||||
|
||||
const date = this.timeService.date;
|
||||
const created = await this.rolesRepository.insertOne({
|
||||
id: this.idService.gen(date.getTime()),
|
||||
|
|
@ -792,6 +887,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
|
||||
@bindThis
|
||||
public async update(role: MiRole, params: Partial<MiRole>, moderator?: MiUser): Promise<void> {
|
||||
this.assertValidRole(params);
|
||||
|
||||
const date = this.timeService.date;
|
||||
await this.rolesRepository.update(role.id, {
|
||||
updatedAt: date,
|
||||
|
|
@ -843,4 +940,30 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public assertValidRole(role: Partial<MiRole>): void {
|
||||
if (!role.policies) return;
|
||||
|
||||
if (this.roleValidator(role.policies)) return;
|
||||
|
||||
throw new IdentifiableError(
|
||||
'39d78ad7-0f00-4bff-b2e2-2e7db889e05d',
|
||||
'invalid policy values',
|
||||
false,
|
||||
this.roleValidator.errors,
|
||||
);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public assertValidDefaultPolicies(policies: object): void {
|
||||
if (this.defaultPoliciesValidator(policies)) return;
|
||||
|
||||
throw new IdentifiableError(
|
||||
'39d78ad7-0f00-4bff-b2e2-2e7db889e05d',
|
||||
'invalid policy values',
|
||||
false,
|
||||
this.defaultPoliciesValidator.errors,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { RoleEntityService } from '@/core/entities/RoleEntityService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin', 'role'],
|
||||
|
|
@ -20,6 +22,14 @@ export const meta = {
|
|||
optional: false, nullable: false,
|
||||
ref: 'Role',
|
||||
},
|
||||
|
||||
errors: {
|
||||
badValues: {
|
||||
message: 'Invalid policy values',
|
||||
code: 'BAD_POLICY_VALUES',
|
||||
id: '39d78ad7-0f00-4bff-b2e2-2e7db889e05d',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
|
@ -69,7 +79,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
super(meta, paramDef, async (ps, me) => {
|
||||
const created = await this.roleService.create(ps, me);
|
||||
|
||||
try {
|
||||
return await this.roleEntityService.pack(created, me);
|
||||
} catch (e) {
|
||||
if (e instanceof IdentifiableError) {
|
||||
if (e.id === '39d78ad7-0f00-4bff-b2e2-2e7db889e05d') {
|
||||
throw new ApiError(
|
||||
meta.errors.badValues,
|
||||
e.cause,
|
||||
);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
|||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin', 'role'],
|
||||
|
|
@ -15,6 +18,14 @@ export const meta = {
|
|||
requireCredential: true,
|
||||
requireAdmin: true,
|
||||
kind: 'write:admin:roles',
|
||||
|
||||
errors: {
|
||||
badValues: {
|
||||
message: 'Invalid policy values',
|
||||
code: 'BAD_POLICY_VALUES',
|
||||
id: '39d78ad7-0f00-4bff-b2e2-2e7db889e05d',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
|
@ -35,8 +46,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private metaService: MetaService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
try {
|
||||
this.roleService.assertValidDefaultPolicies(ps.policies);
|
||||
} catch (e) {
|
||||
if (e instanceof IdentifiableError) {
|
||||
if (e.id === '39d78ad7-0f00-4bff-b2e2-2e7db889e05d') {
|
||||
throw new ApiError(
|
||||
meta.errors.badValues,
|
||||
e.cause,
|
||||
);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
const before = await this.metaService.fetch(true);
|
||||
|
||||
await this.metaService.update({
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import type { RolesRepository } from '@/models/_.js';
|
|||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin', 'role'],
|
||||
|
|
@ -23,6 +24,12 @@ export const meta = {
|
|||
code: 'NO_SUCH_ROLE',
|
||||
id: 'cd23ef55-09ad-428a-ac61-95a45e124b32',
|
||||
},
|
||||
|
||||
badValues: {
|
||||
message: 'Invalid policy values',
|
||||
code: 'BAD_POLICY_VALUES',
|
||||
id: '39d78ad7-0f00-4bff-b2e2-2e7db889e05d',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
|
@ -67,6 +74,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.noSuchRole);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.roleService.update(role, {
|
||||
name: ps.name,
|
||||
description: ps.description,
|
||||
|
|
@ -84,6 +92,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
displayOrder: ps.displayOrder,
|
||||
policies: ps.policies,
|
||||
}, me);
|
||||
} catch (e) {
|
||||
if (e instanceof IdentifiableError) {
|
||||
if (e.id === '39d78ad7-0f00-4bff-b2e2-2e7db889e05d') {
|
||||
throw new ApiError(
|
||||
meta.errors.badValues,
|
||||
e.cause,
|
||||
);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue