feat(world-builder): permissions and protected map restrictions for map editing (#4) - #129
Conversation
| @@ -976,16 +1039,22 @@ app.post("/admin/game-data/maps/:mapNum/publish", async (request, response) => { | |||
| /** Descarta los borradores sin tocar lo ya publicado. */ | |||
| app.post("/admin/game-data/maps/:mapNum/discard", async (request, response) => { | |||
There was a problem hiding this comment.
⚠️ Security: Collaborators can run destructive revert/discard on maps
The publish, discard and revert endpoints now gate only on requireMapEditSession, which passes for any non-superadmin collaborator that holds a game_map_permissions row (including a global map_num = 0 grant). revertMap deletes ALL tiles (published + drafts — the described "panic button") and discardDrafts wipes drafts. This contradicts the stated intent of granting map-level access "without granting global destructive permissions": a builder given one map (or global) can now destroy published content. Consider requiring authorized.isSuperAdmin (or a distinct permission flag) for the revert and discard routes rather than treating them like a normal tile paint.
Was this helpful? React with 👍 / 👎
| CREATE TABLE IF NOT EXISTS game_map_permissions ( | ||
| account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, | ||
| map_num INTEGER NOT NULL CHECK (map_num >= 0), | ||
| granted_by UUID REFERENCES accounts(id) ON DELETE SET NULL, | ||
| created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), | ||
| PRIMARY KEY (account_id, map_num) | ||
| ); | ||
|
|
||
| CREATE INDEX IF NOT EXISTS idx_game_map_permissions_account_map | ||
| ON game_map_permissions(account_id, map_num); |
There was a problem hiding this comment.
💡 Quality: Redundant index duplicates game_map_permissions primary key
idx_game_map_permissions_account_map indexes (account_id, map_num), which is identical (same columns, same order) to the PRIMARY KEY (account_id, map_num) that Postgres already backs with a unique btree index. The extra index adds write overhead and storage without benefiting any query. Drop the explicit index.
Remove the duplicate index; the primary key already covers these lookups.:
CREATE TABLE IF NOT EXISTS game_map_permissions (
account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
map_num INTEGER NOT NULL CHECK (map_num >= 0),
granted_by UUID REFERENCES accounts(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (account_id, map_num)
);
-- (removed redundant idx_game_map_permissions_account_map)
- Apply fix
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎
| app.post( | ||
| "/admin/game-data/maps/:mapNum/permissions/:accountId", | ||
| async (request, response) => { | ||
| try { | ||
| const authorized = await requireAdminEmailSession( | ||
| request, | ||
| response, | ||
| ); | ||
| if (!authorized) return; | ||
|
|
||
| const mapNum = Number.parseInt(request.params.mapNum ?? "", 10); | ||
| const accountId = request.params.accountId ?? ""; | ||
|
|
||
| if (!Number.isInteger(mapNum) || mapNum < 0 || !accountId) { | ||
| response.status(400).json({ error: "Parametros invalidos." }); |
There was a problem hiding this comment.
💡 Quality: Granting a protected map to a collaborator is silently useless
grantMapPermission accepts any mapNum >= 0, including protected maps (1, 34, 59, 150). But checkMapEditPermission rejects protected maps for non-superadmins before consulting game_map_permissions, so such a grant can never take effect and the admin gets a misleading { ok: true }. Reject grants for protected map numbers (or return a warning) so operators aren't misled.
Was this helpful? React with 👍 / 👎
| it("rejects unauthorized accounts without permissions with 403", async () => { | ||
| const result = await checkMapEditPermission({ | ||
| accountId: unauthorizedAccountId, | ||
| isSuperAdmin: false, | ||
| mapNum: 50, | ||
| }); | ||
|
|
||
| assert.equal(result.allowed, false); | ||
| assert.match(result.reason ?? "", /no tiene permisos/i); | ||
| }); |
There was a problem hiding this comment.
💡 Quality: Permission integration tests lack DB setup and positive collaborator case
worldBuilder_permissions.integration.test.ts calls checkMapEditPermission for the unauthorized account (mapNum 50), which executes a live pool.query against game_map_permissions, yet the file has no DB setup/seeding/teardown — the test will throw on connection failure and there is no positive test seeding a collaborator grant to verify the allowed: true collaborator path. Add fixture setup (or mock the pool) and a granted-collaborator assertion.
Was this helpful? React with 👍 / 👎
Code Review
|
| Auto-apply | Compact |
|
|
Important
Your trial ends in 7 days — upgrade now to keep code review, CI analysis, auto-apply, custom automations, and more.
Was this helpful? React with 👍 / 👎 | Gitar
Closes #4
Summary of Changes
Implements granular map editing permissions, protected capital/city map safeguards, account mutation attribution, and permission management endpoints.
Deliverables & Architectural Features
1, Nix34, Banderbill59, Lindos150) asPROTECTED_MAPS. Protected maps reject modifications unless superadmins explicitly passoverrideProtected = true.game_map_permissionstable inapi/schema.sqlenabling map-level assignment for builders/collaborators without granting global destructive permissions.requireMapEditSessionand recordsupdated_by_account_id.POST /admin/game-data/maps/:mapNum/permissions/:accountId), revoke (DELETE), and list (GET) map permissions.GAME_DATA_ADMIN_*configuration inapi/.env.example.api/src/tests/worldBuilder_permissions.integration.test.ts.