From ee6e43b552c4de72a1adc595f7ec8d98ea46d29d Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Sun, 21 Jun 2020 01:22:55 -0700 Subject: [PATCH] Strongly type RoomPermission and GlobalPermission `user-groups.ts` now tracks the list of possible values for `RoomPermission` and `GlobalPermission` in a const. --- server/chat-commands/moderation.ts | 4 +-- server/chat.ts | 13 +++++++--- server/config-loader.ts | 14 ++--------- server/user-groups.ts | 39 +++++++++++++++++++++++++++--- server/users.ts | 6 +++-- 5 files changed, 54 insertions(+), 22 deletions(-) diff --git a/server/chat-commands/moderation.ts b/server/chat-commands/moderation.ts index 3428733139..4c142da1c7 100644 --- a/server/chat-commands/moderation.ts +++ b/server/chat-commands/moderation.ts @@ -115,10 +115,10 @@ export const commands: ChatCommands = { return this.errorReply(`User '${name}' is already a ${nextGroupName} in this room.`); } if (!user.can('makeroom')) { - if (currentGroup.id && !user.can(`room${currentGroup.id || 'voice'}`, null, room)) { + if (currentGroup.id && !user.can(`room${currentGroup.id || 'voice'}` as 'roomvoice', null, room)) { return this.errorReply(`/${cmd} - Access denied for promoting/demoting from ${currentGroupName}.`); } - if (nextSymbol !== ' ' && !user.can(`room${nextGroup.id || 'voice'}`, null, room)) { + if (nextSymbol !== ' ' && !user.can(`room${nextGroup.id || 'voice'}` as 'roomvoice', null, room)) { return this.errorReply(`/${cmd} - Access denied for promoting/demoting to ${nextGroupName}.`); } } diff --git a/server/chat.ts b/server/chat.ts index 833303967b..6ba20e0e5b 100644 --- a/server/chat.ts +++ b/server/chat.ts @@ -23,6 +23,9 @@ To reload chat commands: */ +type RoomPermission = import('./user-groups').RoomPermission; +type GlobalPermission = import('./user-groups').GlobalPermission; + export type PageHandler = (this: PageContext, query: string[], user: User, connection: Connection) => Promise | string | null | void; export interface PageTable { @@ -48,7 +51,7 @@ export type SettingsHandler = ( connection: Connection ) => { label: string, - permission: boolean | string, + permission: boolean | RoomPermission | GlobalPermission, // button label, command | disabled options: [string, string | true][], }; @@ -215,8 +218,10 @@ export class PageContext extends MessageContext { this.title = 'Page'; } + can(permission: RoomPermission | GlobalPermission, target?: User | null, room?: Room | null): boolean; + can(permission: GlobalPermission, target?: User | null): boolean; can(permission: string, target: User | null = null, room: Room | null = null) { - if (!this.user.can(permission, target, room)) { + if (!this.user.can(permission as any, target, room)) { this.send(`

Permission denied.

`); return false; } @@ -742,8 +747,10 @@ export class CommandContext extends MessageContext { statusfilter(status: string) { return Chat.statusfilter(status, this.user); } + can(permission: RoomPermission | GlobalPermission, target?: User | null, room?: Room | null): boolean; + can(permission: GlobalPermission, target?: User | null): boolean; can(permission: string, target: User | null = null, room: Room | null = null) { - if (!this.user.can(permission, target, room)) { + if (!this.user.can(permission as any, target, room)) { this.errorReply(this.cmdToken + this.fullCmd + " - Access denied."); return false; } diff --git a/server/config-loader.ts b/server/config-loader.ts index 2c2c464476..076af38a0f 100644 --- a/server/config-loader.ts +++ b/server/config-loader.ts @@ -6,17 +6,7 @@ */ import * as defaults from '../config/config-example'; - -export interface GroupInfo { - symbol: GroupSymbol; - id: ID; - name: string; - rank: number; - inherit?: GroupSymbol; - jurisdiction?: string; - globalGroupInPersonalRoom?: GroupSymbol; - [k: string]: string | true | number | undefined; -} +type GroupInfo = import('./user-groups').GroupInfo; export type ConfigType = typeof defaults & { groups: {[symbol: string]: GroupInfo}, @@ -68,7 +58,7 @@ export function cacheGroupData(config: ConfigType) { // preserving permissions specifically declared for the higher group. for (const key in inheritGroup) { if (key in groupData) continue; - groupData[key] = inheritGroup[key]; + (groupData as any)[key] = (inheritGroup as any)[key]; } } delete groupData['inherit']; diff --git a/server/user-groups.ts b/server/user-groups.ts index a6a9662ca2..4178fa66d5 100644 --- a/server/user-groups.ts +++ b/server/user-groups.ts @@ -1,9 +1,39 @@ import {FS} from '../lib/fs'; -type GroupInfo = import('./config-loader').GroupInfo; export const PLAYER_SYMBOL: GroupSymbol = '\u2606'; export const HOST_SYMBOL: GroupSymbol = '\u2605'; +export const ROOM_PERMISSIONS = [ + 'addhtml', 'announce', 'ban', 'broadcast', 'bypassafktimer', 'declare', 'editprivacy', 'editroom', 'exportinputlog', 'game', 'gamemanagement', 'gamemoderation', 'joinbattle', 'kick', 'minigame', 'modchat', 'modchatall', 'modlog', 'mute', 'nooverride', 'receiveauthmessages', 'roombot', 'roomdriver', 'roommod', 'roomowner', 'roomvoice', 'showmedia', 'timer', 'tournaments', 'warn', +] as const; + +export const GLOBAL_PERMISSIONS = [ + // administrative + 'bypassall', 'console', 'disableladder', 'lockdown', 'potd', 'rawpacket', + // other + 'alts', 'autotimer', 'bypassblocks', 'forcepromote', 'forcerename', 'forcewin', 'gdeclare', 'ignorelimits', 'ip', 'lock', 'makeroom', 'rangeban', 'promote', +] as const; + +export type RoomPermission = typeof ROOM_PERMISSIONS[number]; +export type GlobalPermission = typeof GLOBAL_PERMISSIONS[number]; + +export type GroupInfo = { + symbol: GroupSymbol, + id: ID, + name: string, + rank: number, + inherit?: GroupSymbol, + jurisdiction?: string, + + globalonly?: boolean, + roomonly?: boolean, + battleonly?: boolean, + root?: boolean, + globalGroupInPersonalRoom?: GroupSymbol, +} & { + [P in RoomPermission | GlobalPermission]?: string | boolean; +}; + /** * Auth table - a Map for which users are in which groups. * @@ -49,7 +79,10 @@ export abstract class Auth extends Map { }); } static hasPermission( - symbol: GroupSymbol, permission: string, targetSymbol?: GroupSymbol, targetingSelf?: boolean + symbol: GroupSymbol, + permission: GlobalPermission | RoomPermission | 'jurisdiction', + targetSymbol?: GroupSymbol, + targetingSelf?: boolean ): boolean { const group = Auth.getGroup(symbol); if (group['root']) { @@ -80,7 +113,7 @@ export abstract class Auth extends Map { } return false; } - static listJurisdiction(symbol: GroupSymbol, permission: string) { + static listJurisdiction(symbol: GroupSymbol, permission: GlobalPermission | RoomPermission) { const symbols = Object.keys(Config.groups) as GroupSymbol[]; return symbols.filter(targetSymbol => Auth.hasPermission(symbol, permission, targetSymbol)); } diff --git a/server/users.ts b/server/users.ts index 506cb78056..b98acd8e7c 100644 --- a/server/users.ts +++ b/server/users.ts @@ -41,7 +41,7 @@ const PERMALOCK_CACHE_TIME = 30 * 24 * 60 * 60 * 1000; // 30 days const DEFAULT_TRAINER_SPRITES = [1, 2, 101, 102, 169, 170, 265, 266]; import {FS} from '../lib/fs'; -import {Auth, GlobalAuth, PLAYER_SYMBOL, HOST_SYMBOL} from './user-groups'; +import {Auth, GlobalAuth, PLAYER_SYMBOL, HOST_SYMBOL, RoomPermission, GlobalPermission} from './user-groups'; const MINUTES = 60 * 1000; const IDLE_TIMER = 60 * MINUTES; @@ -520,6 +520,8 @@ export class User extends Chat.MessageContext { const auth = (room && !this.can('makeroom') ? room.auth.get(this.id) : this.group); return auth in Config.groups && Config.groups[auth].rank >= Config.groups[minAuth].rank; } + can(permission: RoomPermission | GlobalPermission, target?: User | null, room?: Room | BasicChatRoom | null): boolean; + can(permission: GlobalPermission, target?: User | null): boolean; can(permission: string, target: User | null = null, room: Room | BasicChatRoom | null = null): boolean { if (this.hasSysopAccess()) return true; @@ -537,7 +539,7 @@ export class User extends Chat.MessageContext { if (replaceGroup) group = replaceGroup; } - return Auth.hasPermission(group, permission, targetGroup, target === this); + return Auth.hasPermission(group, permission as any, targetGroup, target === this); } /** * Special permission check for system operators