mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 11:50:42 +00:00
custom flag (2) (#1303)
## Description: This PR implements the permission check logic. Other related parts will be handled in a separate UI update. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors
This commit is contained in:
@@ -123,98 +123,406 @@
|
||||
},
|
||||
"flag": {
|
||||
"layers": {
|
||||
"a": { "name": "center_circle" },
|
||||
"b": { "name": "center_hline" },
|
||||
"c": { "name": "center_vline" },
|
||||
"d": { "name": "center_star" },
|
||||
"e": { "name": "center_flower" },
|
||||
"f": { "name": "flower_tl" },
|
||||
"g": { "name": "flower_tc" },
|
||||
"h": { "name": "flower_tr" },
|
||||
"i": { "name": "diag_br" },
|
||||
"j": { "name": "diag_bl" },
|
||||
"k": { "name": "frame" },
|
||||
"l": { "name": "full" },
|
||||
"m": { "name": "triangle_tl" },
|
||||
"n": { "name": "triangle_bl" },
|
||||
"o": { "name": "triangle_tr" },
|
||||
"p": { "name": "triangle_br" },
|
||||
"q": { "name": "half_l" },
|
||||
"r": { "name": "half_r" },
|
||||
"s": { "name": "half_t" },
|
||||
"t": { "name": "half_b" },
|
||||
"u": { "name": "mini_tr_bl" },
|
||||
"v": { "name": "mini_tr_br" },
|
||||
"w": { "name": "mini_tr_tl" },
|
||||
"x": { "name": "mini_tr_tr" },
|
||||
"y": { "name": "triangle_t" },
|
||||
"z": { "name": "triangle_l" },
|
||||
"aa": { "name": "triangle_b" },
|
||||
"ab": { "name": "triangle_r" },
|
||||
"ac": { "name": "tricolor_l" },
|
||||
"ad": { "name": "tricolor_c" },
|
||||
"ae": { "name": "tricolor_r" },
|
||||
"af": { "name": "tricolor_t" },
|
||||
"ag": { "name": "tricolor_m" },
|
||||
"ah": { "name": "tricolor_b" },
|
||||
"ai": { "name": "nato_emblem" },
|
||||
"aj": { "name": "eu_star" },
|
||||
"ak": { "name": "laurel_wreath" },
|
||||
"al": { "name": "ofm_2025" },
|
||||
"am": { "name": "octagram" },
|
||||
"an": { "name": "octagram_2" },
|
||||
"ao": { "name": "og" },
|
||||
"ap": { "name": "og_plus" },
|
||||
"aq": { "name": "beta_tester" },
|
||||
"ar": { "name": "beta_tester_circle" },
|
||||
"as": { "name": "rocket" },
|
||||
"at": { "name": "rocket_mini" },
|
||||
"au": { "name": "translator" },
|
||||
"av": { "name": "admin_shield" },
|
||||
"aw": { "name": "admin_shield_r" },
|
||||
"ax": { "name": "admin_evan" }
|
||||
"a": {
|
||||
"name": "center_circle",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"b": {
|
||||
"name": "center_hline",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"c": {
|
||||
"name": "center_vline",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"d": {
|
||||
"name": "center_star",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"e": {
|
||||
"name": "center_flower",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"f": {
|
||||
"name": "flower_tl",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"g": {
|
||||
"name": "flower_tc",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"h": {
|
||||
"name": "flower_tr",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"i": {
|
||||
"name": "diag_br",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"j": {
|
||||
"name": "diag_bl",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"k": {
|
||||
"name": "frame"
|
||||
},
|
||||
"l": {
|
||||
"name": "full"
|
||||
},
|
||||
"m": {
|
||||
"name": "triangle_tl",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"n": {
|
||||
"name": "triangle_bl",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"o": {
|
||||
"name": "triangle_tr",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"p": {
|
||||
"name": "triangle_br",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"q": {
|
||||
"name": "half_l",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"r": {
|
||||
"name": "half_r",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"s": {
|
||||
"name": "half_t",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"t": {
|
||||
"name": "half_b",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"u": {
|
||||
"name": "mini_tr_bl",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"v": {
|
||||
"name": "mini_tr_br",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"w": {
|
||||
"name": "mini_tr_tl",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"x": {
|
||||
"name": "mini_tr_tr",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"y": {
|
||||
"name": "triangle_t",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"z": {
|
||||
"name": "triangle_l",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"aa": {
|
||||
"name": "triangle_b",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"ab": {
|
||||
"name": "triangle_r",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"ac": {
|
||||
"name": "tricolor_l",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"ad": {
|
||||
"name": "tricolor_c",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"ae": {
|
||||
"name": "tricolor_r",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"af": {
|
||||
"name": "tricolor_t",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"ag": {
|
||||
"name": "tricolor_m",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"ah": {
|
||||
"name": "tricolor_b",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"ai": {
|
||||
"name": "nato_emblem",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"aj": {
|
||||
"name": "eu_star",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"ak": {
|
||||
"name": "laurel_wreath",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"al": {
|
||||
"name": "ofm_2025",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"am": {
|
||||
"name": "octagram",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"an": {
|
||||
"name": "octagram_2",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"ao": {
|
||||
"name": "og",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"ap": {
|
||||
"name": "og_plus",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"aq": {
|
||||
"name": "beta_tester",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"ar": {
|
||||
"name": "beta_tester_circle",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"as": {
|
||||
"name": "rocket",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"at": {
|
||||
"name": "rocket_mini",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"au": {
|
||||
"name": "translator",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"av": {
|
||||
"name": "admin_shield",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"aw": {
|
||||
"name": "admin_shield_r",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"ax": {
|
||||
"name": "admin_evan",
|
||||
"role_group": "donor"
|
||||
}
|
||||
},
|
||||
"color": {
|
||||
"a": { "color": "#ff0000", "name": "red" },
|
||||
"b": { "color": "#ffa500", "name": "orange" },
|
||||
"c": { "color": "#ffff00", "name": "yellow" },
|
||||
"d": { "color": "#008000", "name": "green" },
|
||||
"e": { "color": "#00ffff", "name": "cyan" },
|
||||
"f": { "color": "#0000ff", "name": "blue" },
|
||||
"g": { "color": "#000000", "name": "black" },
|
||||
"h": { "color": "#ffffff", "name": "white" },
|
||||
"i": { "color": "#800080", "name": "purple" },
|
||||
"j": { "color": "#ff69b4", "name": "hotpink" },
|
||||
"k": { "color": "#a52a2a", "name": "brown" },
|
||||
"l": { "color": "#808080", "name": "gray" },
|
||||
"m": { "color": "#20b2aa", "name": "teal" },
|
||||
"n": { "color": "#ff6347", "name": "tomato" },
|
||||
"o": { "color": "#4682b4", "name": "steelblue" },
|
||||
"p": { "color": "#90ee90", "name": "lightgreen" },
|
||||
"q": { "color": "#8b0000", "name": "darkred" },
|
||||
"r": { "color": "#191970", "name": "navy" },
|
||||
"s": { "color": "#ffd700", "name": "gold" },
|
||||
"t": { "color": "#add8e6", "name": "lightblue" },
|
||||
"u": { "color": "#f5f5dc", "name": "beige" },
|
||||
"v": { "color": "#ffb6c1", "name": "lightpink" },
|
||||
"w": { "color": "#708090", "name": "slategray" },
|
||||
"x": { "color": "#00ff7f", "name": "springgreen" },
|
||||
"y": { "color": "#dc143c", "name": "crimson" },
|
||||
"z": { "color": "#ffbf00", "name": "amber" },
|
||||
"0": { "color": "#3d9970", "name": "olive_green" },
|
||||
"1": { "color": "#87ceeb", "name": "sky_blue" },
|
||||
"2": { "color": "#6a5acd", "name": "slate_blue" },
|
||||
"3": { "color": "#ff66cc", "name": "rose_pink" },
|
||||
"4": { "color": "#36454f", "name": "charcoal" },
|
||||
"5": { "color": "#fffff0", "name": "ivory" },
|
||||
"A": { "color": "rainbow", "name": "rainbow" },
|
||||
"B": { "color": "bright-rainbow", "name": "bright_rainbow" },
|
||||
"C": { "color": "gold-glow", "name": "gold_glow" },
|
||||
"D": { "color": "silver-glow", "name": "silver_glow" },
|
||||
"E": { "color": "copper-glow", "name": "copper_glow" },
|
||||
"F": { "color": "neon", "name": "neon" },
|
||||
"G": { "color": "lava", "name": "lava" },
|
||||
"H": { "color": "water", "name": "water" }
|
||||
"a": {
|
||||
"color": "#ff0000",
|
||||
"name": "red",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"b": {
|
||||
"color": "#ffa500",
|
||||
"name": "orange",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"c": {
|
||||
"color": "#ffff00",
|
||||
"name": "yellow",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"d": {
|
||||
"color": "#008000",
|
||||
"name": "green",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"e": {
|
||||
"color": "#00ffff",
|
||||
"name": "cyan",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"f": {
|
||||
"color": "#0000ff",
|
||||
"name": "blue",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"g": {
|
||||
"color": "#000000",
|
||||
"name": "black",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"h": {
|
||||
"color": "#ffffff",
|
||||
"name": "white",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"i": {
|
||||
"color": "#800080",
|
||||
"name": "purple",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"j": {
|
||||
"color": "#ff69b4",
|
||||
"name": "hotpink",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"k": {
|
||||
"color": "#a52a2a",
|
||||
"name": "brown",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"l": {
|
||||
"color": "#808080",
|
||||
"name": "gray",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"m": {
|
||||
"color": "#20b2aa",
|
||||
"name": "teal",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"n": {
|
||||
"color": "#ff6347",
|
||||
"name": "tomato",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"o": {
|
||||
"color": "#4682b4",
|
||||
"name": "steelblue",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"p": {
|
||||
"color": "#90ee90",
|
||||
"name": "lightgreen",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"q": {
|
||||
"color": "#8b0000",
|
||||
"name": "darkred",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"r": {
|
||||
"color": "#191970",
|
||||
"name": "navy",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"s": {
|
||||
"color": "#ffd700",
|
||||
"name": "gold",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"t": {
|
||||
"color": "#add8e6",
|
||||
"name": "lightblue",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"u": {
|
||||
"color": "#f5f5dc",
|
||||
"name": "beige",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"v": {
|
||||
"color": "#ffb6c1",
|
||||
"name": "lightpink",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"w": {
|
||||
"color": "#708090",
|
||||
"name": "slategray",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"x": {
|
||||
"color": "#00ff7f",
|
||||
"name": "springgreen",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"y": {
|
||||
"color": "#dc143c",
|
||||
"name": "crimson",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"z": {
|
||||
"color": "#ffbf00",
|
||||
"name": "amber",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"0": {
|
||||
"color": "#3d9970",
|
||||
"name": "olive_green",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"1": {
|
||||
"color": "#87ceeb",
|
||||
"name": "sky_blue",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"2": {
|
||||
"color": "#6a5acd",
|
||||
"name": "slate_blue",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"3": {
|
||||
"color": "#ff66cc",
|
||||
"name": "rose_pink",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"4": {
|
||||
"color": "#36454f",
|
||||
"name": "charcoal",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"5": {
|
||||
"color": "#fffff0",
|
||||
"name": "ivory",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"A": {
|
||||
"color": "rainbow",
|
||||
"name": "rainbow",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"B": {
|
||||
"color": "bright-rainbow",
|
||||
"name": "bright_rainbow",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"C": {
|
||||
"color": "gold-glow",
|
||||
"name": "gold_glow",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"D": {
|
||||
"color": "silver-glow",
|
||||
"name": "silver_glow",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"E": {
|
||||
"color": "copper-glow",
|
||||
"name": "copper_glow",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"F": {
|
||||
"color": "neon",
|
||||
"name": "neon",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"G": {
|
||||
"color": "lava",
|
||||
"name": "lava",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"H": {
|
||||
"color": "water",
|
||||
"name": "water",
|
||||
"role_group": "donor"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { base64url } from "jose";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, LitElement, render } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
@@ -369,7 +370,10 @@ export function generatePreviewDataUrl(
|
||||
height?: number,
|
||||
): string {
|
||||
// Calculate canvas size
|
||||
const decoder = new PatternDecoder(pattern ?? DEFAULT_PATTERN_B64);
|
||||
const decoder = new PatternDecoder(
|
||||
pattern ?? DEFAULT_PATTERN_B64,
|
||||
base64url.decode,
|
||||
);
|
||||
const scaledWidth = decoder.scaledWidth();
|
||||
const scaledHeight = decoder.scaledHeight();
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ export const CosmeticsSchema = z.object({
|
||||
z.string(),
|
||||
z.object({
|
||||
name: z.string(),
|
||||
role_group: z.string().optional(),
|
||||
flares: z.array(z.string()).optional(),
|
||||
}),
|
||||
),
|
||||
color: z.record(
|
||||
@@ -24,6 +26,8 @@ export const CosmeticsSchema = z.object({
|
||||
z.object({
|
||||
color: z.string(),
|
||||
name: z.string(),
|
||||
role_group: z.string().optional(),
|
||||
flares: z.array(z.string()).optional(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { base64url } from "jose";
|
||||
|
||||
export class PatternDecoder {
|
||||
private bytes: Uint8Array;
|
||||
|
||||
@@ -7,8 +5,11 @@ export class PatternDecoder {
|
||||
readonly width: number;
|
||||
readonly scale: number;
|
||||
|
||||
constructor(base64: string) {
|
||||
this.bytes = base64url.decode(base64);
|
||||
constructor(
|
||||
base64: string,
|
||||
base64urlDecode: (input: Uint8Array | string) => Uint8Array,
|
||||
) {
|
||||
this.bytes = base64urlDecode(base64);
|
||||
|
||||
if (this.bytes.length < 3) {
|
||||
throw new Error(
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
import { base64url } from "jose";
|
||||
import { z } from "zod/v4";
|
||||
import quickChatData from "../../resources/QuickChat.json" with { type: "json" };
|
||||
import {
|
||||
@@ -190,7 +191,7 @@ export const RequiredPatternSchema = z
|
||||
.refine(
|
||||
(val) => {
|
||||
try {
|
||||
new PatternDecoder(val);
|
||||
new PatternDecoder(val, base64url.decode);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify(e.message, null, 2));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { base64url } from "jose";
|
||||
import { Config } from "../configuration/Config";
|
||||
import { PatternDecoder } from "../PatternDecoder";
|
||||
import { ClientID, GameID, Player } from "../Schemas";
|
||||
@@ -163,7 +164,7 @@ export class PlayerView {
|
||||
this.decoder =
|
||||
this.cosmetics.pattern === undefined
|
||||
? undefined
|
||||
: new PatternDecoder(this.cosmetics.pattern);
|
||||
: new PatternDecoder(this.cosmetics.pattern, base64url.decode);
|
||||
}
|
||||
|
||||
patternDecoder(): PatternDecoder | undefined {
|
||||
|
||||
+95
-2
@@ -2,7 +2,10 @@ import { Cosmetics } from "../core/CosmeticSchemas";
|
||||
import { PatternDecoder } from "../core/PatternDecoder";
|
||||
|
||||
export class PrivilegeChecker {
|
||||
constructor(private cosmetics: Cosmetics) {}
|
||||
constructor(
|
||||
private cosmetics: Cosmetics,
|
||||
private b64urlDecode: (base64: string) => Uint8Array,
|
||||
) {}
|
||||
|
||||
isPatternAllowed(
|
||||
base64: string,
|
||||
@@ -14,7 +17,7 @@ export class PrivilegeChecker {
|
||||
if (found === undefined) {
|
||||
try {
|
||||
// Ensure that the pattern will not throw for clients
|
||||
new PatternDecoder(base64);
|
||||
new PatternDecoder(base64, this.b64urlDecode);
|
||||
} catch (e) {
|
||||
// Pattern is invalid
|
||||
return "invalid";
|
||||
@@ -55,4 +58,94 @@ export class PrivilegeChecker {
|
||||
|
||||
return "restricted";
|
||||
}
|
||||
|
||||
isCustomFlagAllowed(
|
||||
flag: string,
|
||||
roles: readonly string[] | undefined,
|
||||
flares: readonly string[] | undefined,
|
||||
): true | "restricted" | "invalid" {
|
||||
if (!flag.startsWith("!")) return "invalid";
|
||||
const code = flag.slice(1);
|
||||
if (!code) return "invalid";
|
||||
const segments = code.split("_");
|
||||
if (segments.length === 0) return "invalid";
|
||||
|
||||
const MAX_LAYERS = 6; // Maximum number of layers allowed
|
||||
if (segments.length > MAX_LAYERS) return "invalid";
|
||||
|
||||
const superFlare = flares?.includes("flag:*") ?? false;
|
||||
|
||||
for (const segment of segments) {
|
||||
const [layerKey, colorKey] = segment.split("-");
|
||||
if (!layerKey || !colorKey) return "invalid";
|
||||
const layer = this.cosmetics.flag.layers[layerKey];
|
||||
const color = this.cosmetics.flag.color[colorKey];
|
||||
if (!layer || !color) return "invalid";
|
||||
|
||||
// Super-flare bypasses all restrictions
|
||||
if (superFlare) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check layer restrictions
|
||||
const layerSpec = layer;
|
||||
let layerAllowed = false;
|
||||
if (!layerSpec.role_group && !layerSpec.flares) {
|
||||
layerAllowed = true;
|
||||
} else {
|
||||
// By role
|
||||
if (layerSpec.role_group) {
|
||||
const allowedRoles =
|
||||
this.cosmetics.role_groups[layerSpec.role_group] || [];
|
||||
if (roles?.some((r) => allowedRoles.includes(r))) {
|
||||
layerAllowed = true;
|
||||
}
|
||||
}
|
||||
// By flare
|
||||
if (
|
||||
layerSpec.flares &&
|
||||
flares?.some((f) => layerSpec.flares?.includes(f))
|
||||
) {
|
||||
layerAllowed = true;
|
||||
}
|
||||
// By named flag:layer:{name}
|
||||
if (flares?.includes(`flag:layer:${layerSpec.name}`)) {
|
||||
layerAllowed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check color restrictions
|
||||
const colorSpec = color;
|
||||
let colorAllowed = false;
|
||||
if (!colorSpec.role_group && !colorSpec.flares) {
|
||||
colorAllowed = true;
|
||||
} else {
|
||||
// By role
|
||||
if (colorSpec.role_group) {
|
||||
const allowedRoles =
|
||||
this.cosmetics.role_groups[colorSpec.role_group] || [];
|
||||
if (roles?.some((r) => allowedRoles.includes(r))) {
|
||||
colorAllowed = true;
|
||||
}
|
||||
}
|
||||
// By flare
|
||||
if (
|
||||
colorSpec.flares &&
|
||||
flares?.some((f) => colorSpec.flares?.includes(f))
|
||||
) {
|
||||
colorAllowed = true;
|
||||
}
|
||||
// By named flag:color:{name}
|
||||
if (flares?.includes(`flag:color:${colorSpec.name}`)) {
|
||||
colorAllowed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If either part is restricted, block
|
||||
if (!(layerAllowed && colorAllowed)) {
|
||||
return "restricted";
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+14
-2
@@ -2,6 +2,7 @@ import express, { NextFunction, Request, Response } from "express";
|
||||
import rateLimit from "express-rate-limit";
|
||||
import http from "http";
|
||||
import ipAnonymize from "ip-anonymize";
|
||||
import { base64url } from "jose";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { WebSocket, WebSocketServer } from "ws";
|
||||
@@ -44,7 +45,7 @@ export function startWorker() {
|
||||
|
||||
const gm = new GameManager(config, log);
|
||||
|
||||
const privilegeChecker = new PrivilegeChecker(COSMETICS);
|
||||
const privilegeChecker = new PrivilegeChecker(COSMETICS, base64url.decode);
|
||||
|
||||
if (config.otelEnabled()) {
|
||||
initWorkerMetrics(gm);
|
||||
@@ -369,7 +370,18 @@ export function startWorker() {
|
||||
|
||||
// Check if the flag is allowed
|
||||
if (clientMsg.flag !== undefined) {
|
||||
// TODO: Implement custom flag validation
|
||||
if (clientMsg.flag.startsWith("!")) {
|
||||
const allowed = privilegeChecker.isCustomFlagAllowed(
|
||||
clientMsg.flag,
|
||||
roles,
|
||||
flares,
|
||||
);
|
||||
if (allowed !== true) {
|
||||
log.warn(`Custom flag ${allowed}: ${clientMsg.flag}`);
|
||||
ws.close(1002, `Custom flag ${allowed}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the pattern is allowed
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import type { Cosmetics } from "../../src/core/CosmeticSchemas";
|
||||
import { PrivilegeChecker } from "../../src/server/Privilege";
|
||||
|
||||
describe("PrivilegeChecker.isCustomFlagAllowed (with mock cosmetics)", () => {
|
||||
const dummyPatternDecoder = (_base64: string) => {
|
||||
throw new Error("Method not implemented");
|
||||
};
|
||||
|
||||
const mockCosmetics: Cosmetics = {
|
||||
role_groups: {
|
||||
donor: ["role_donor"],
|
||||
admin: ["role_admin"],
|
||||
},
|
||||
patterns: {},
|
||||
flag: {
|
||||
layers: {
|
||||
a: {
|
||||
name: "chocolate",
|
||||
role_group: "donor",
|
||||
flares: ["cosmetic:flags"],
|
||||
},
|
||||
b: { name: "center_hline" },
|
||||
c: { name: "admin_layer", role_group: "admin" },
|
||||
},
|
||||
color: {
|
||||
a: { color: "#ff0000", name: "red", role_group: "admin" },
|
||||
b: { color: "#00ff00", name: "green" },
|
||||
c: { color: "#0000ff", name: "blue", flares: ["cosmetic:blue"] },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const checker = new PrivilegeChecker(mockCosmetics, dummyPatternDecoder);
|
||||
|
||||
it("allowed: unrestricted layer/color", () => {
|
||||
expect(checker.isCustomFlagAllowed("!b-b", [], [])).toBe(true);
|
||||
});
|
||||
|
||||
it("restricted: donor layer without role", () => {
|
||||
expect(checker.isCustomFlagAllowed("!a-b", [], [])).toBe("restricted");
|
||||
});
|
||||
|
||||
it("allowed: donor layer with donor role", () => {
|
||||
expect(checker.isCustomFlagAllowed("!a-b", ["role_donor"], [])).toBe(true);
|
||||
});
|
||||
|
||||
it("allowed: donor layer with correct flare", () => {
|
||||
expect(checker.isCustomFlagAllowed("!a-b", [], ["cosmetic:flags"])).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("restricted: admin color without role", () => {
|
||||
expect(checker.isCustomFlagAllowed("!b-a", [], [])).toBe("restricted");
|
||||
});
|
||||
|
||||
it("allowed: admin color with admin role", () => {
|
||||
expect(checker.isCustomFlagAllowed("!b-a", ["role_admin"], [])).toBe(true);
|
||||
});
|
||||
|
||||
it("allowed: color with correct flare", () => {
|
||||
expect(checker.isCustomFlagAllowed("!b-c", [], ["cosmetic:blue"])).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("invalid: non-existent layer", () => {
|
||||
expect(checker.isCustomFlagAllowed("!zzz-a", ["role_donor"], [])).toBe(
|
||||
"invalid",
|
||||
);
|
||||
});
|
||||
|
||||
it("invalid: non-existent color", () => {
|
||||
expect(checker.isCustomFlagAllowed("!a-zzz", ["role_donor"], [])).toBe(
|
||||
"invalid",
|
||||
);
|
||||
});
|
||||
|
||||
it("allowed: superFlare allows all listed", () => {
|
||||
expect(checker.isCustomFlagAllowed("!a-a", [], ["flag:*"])).toBe(true);
|
||||
expect(checker.isCustomFlagAllowed("!b-b", [], ["flag:*"])).toBe(true);
|
||||
expect(checker.isCustomFlagAllowed("!c-a", [], ["flag:*"])).toBe(true);
|
||||
expect(checker.isCustomFlagAllowed("!a-c", [], ["flag:*"])).toBe(true);
|
||||
});
|
||||
|
||||
it("invalid: superFlare does not allow non-existent", () => {
|
||||
expect(checker.isCustomFlagAllowed("!zzz-zzz", [], ["flag:*"])).toBe(
|
||||
"invalid",
|
||||
);
|
||||
});
|
||||
it("allowed: flare flag:layer:chocolate allows chocolate layer", () => {
|
||||
expect(
|
||||
checker.isCustomFlagAllowed("!a-b", [], ["flag:layer:chocolate"]),
|
||||
).toBe(true);
|
||||
});
|
||||
it("allowed: flare flag:color:blue allows blue color", () => {
|
||||
expect(checker.isCustomFlagAllowed("!b-c", [], ["flag:color:blue"])).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
it("restricted: only color flare, layer still restricted", () => {
|
||||
expect(checker.isCustomFlagAllowed("!a-c", [], ["cosmetic:blue"])).toBe(
|
||||
"restricted",
|
||||
);
|
||||
});
|
||||
it("restricted: only layer flare, color still restricted", () => {
|
||||
expect(checker.isCustomFlagAllowed("!c-a", [], ["cosmetic:flags"])).toBe(
|
||||
"restricted",
|
||||
);
|
||||
});
|
||||
it("allowed: layer by role, color by flare", () => {
|
||||
// layer a: role_group donor, color c: flares ["cosmetic:blue"]
|
||||
expect(
|
||||
checker.isCustomFlagAllowed("!a-c", ["role_donor"], ["cosmetic:blue"]),
|
||||
).toBe(true);
|
||||
});
|
||||
it("restricted: layer by role, color by flare (missing flare)", () => {
|
||||
expect(checker.isCustomFlagAllowed("!a-c", ["role_donor"], [])).toBe(
|
||||
"restricted",
|
||||
);
|
||||
});
|
||||
it("restricted: layer by role, color by flare (missing role)", () => {
|
||||
expect(checker.isCustomFlagAllowed("!a-c", [], ["cosmetic:blue"])).toBe(
|
||||
"restricted",
|
||||
);
|
||||
});
|
||||
it("allowed: layer by flare, color by role", () => {
|
||||
// layer a: flares ["cosmetic:flags"], color a: role_group admin
|
||||
expect(
|
||||
checker.isCustomFlagAllowed("!a-a", ["role_admin"], ["cosmetic:flags"]),
|
||||
).toBe(true);
|
||||
});
|
||||
it("restricted: layer by flare, color by role (missing flare)", () => {
|
||||
expect(checker.isCustomFlagAllowed("!a-a", ["role_admin"], [])).toBe(
|
||||
"restricted",
|
||||
);
|
||||
});
|
||||
it("restricted: layer by flare, color by role (missing role)", () => {
|
||||
expect(checker.isCustomFlagAllowed("!a-a", [], ["cosmetic:flags"])).toBe(
|
||||
"restricted",
|
||||
);
|
||||
});
|
||||
it("allowed: two segments, both unrestricted", () => {
|
||||
expect(checker.isCustomFlagAllowed("!b-b_b-b", [], [])).toBe(true);
|
||||
});
|
||||
it("restricted: two segments, one restricted by layer role", () => {
|
||||
expect(checker.isCustomFlagAllowed("!a-b_b-b", [], [])).toBe("restricted");
|
||||
expect(checker.isCustomFlagAllowed("!a-b_b-b", ["role_donor"], [])).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
it("restricted: two segments, one restricted by color role", () => {
|
||||
expect(checker.isCustomFlagAllowed("!b-a_b-b", [], [])).toBe("restricted");
|
||||
expect(checker.isCustomFlagAllowed("!b-a_b-b", ["role_admin"], [])).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
it("allowed: two segments, one by role, one by flare", () => {
|
||||
expect(
|
||||
checker.isCustomFlagAllowed(
|
||||
"!a-c_b-b",
|
||||
["role_donor"],
|
||||
["cosmetic:blue"],
|
||||
),
|
||||
).toBe(true);
|
||||
expect(checker.isCustomFlagAllowed("!a-c_b-b", ["role_donor"], [])).toBe(
|
||||
"restricted",
|
||||
);
|
||||
expect(checker.isCustomFlagAllowed("!a-c_b-b", [], ["cosmetic:blue"])).toBe(
|
||||
"restricted",
|
||||
);
|
||||
});
|
||||
it("allowed: two segments, both by flare", () => {
|
||||
expect(
|
||||
checker.isCustomFlagAllowed(
|
||||
"!a-c_a-c",
|
||||
[],
|
||||
["cosmetic:flags", "cosmetic:blue"],
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
checker.isCustomFlagAllowed("!a-c_a-c", [], ["cosmetic:flags"]),
|
||||
).toBe("restricted");
|
||||
expect(checker.isCustomFlagAllowed("!a-c_a-c", [], ["cosmetic:blue"])).toBe(
|
||||
"restricted",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user