Skip to content

permission: file stores draw one id sequence - #93

Open
aberoham wants to merge 3 commits into
NicTool:mainfrom
aberoham:permission-file-ids
Open

aberoham wants to merge 3 commits into
NicTool:mainfrom
aberoham:permission-file-ids

Conversation

@aberoham

@aberoham aberoham commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

On json and toml, a permission took its id from its user or group. Group 5 and user 5 both got permission 5, so GET /permission/5 could only answer for one of them. get({ id }) skipped groups to avoid returning the wrong one. A group permission was then unreachable by id, and a user going explicit could reuse an id another permission held.

User, group and standalone permissions now draw from one sequence, kept as the permission file's last_id. That is what nt_perm's auto increment gives mysql. An id given explicitly is recorded too, so it is never handed out again after a destroy.

Stores written before this change repeat ids: a user and a group, a standalone row and an owner, and with an explicit id two users. The server now calls Permission.migrate() before it takes requests, once per store. It starts the sequence past the owners' last_ids and renumbers repeats among users and standalone rows. The row get({ id }) answered on main keeps the id, so an id a client holds still means the same row. Every group permission gets a new id, since main never reached one by id and its old id may be one a removed user permission held. mysql has nothing to migrate.

File stores now hide revoked permissions the way mysql does. get({ uid }), get({ gid }), getGroup, Group.get and User.get all skip them, so POST /zone no longer takes default nameservers from one. A regrant takes a new id, as mysql's delete and insert does.

A standalone create refuses a second permission for one user or group, where mysql returns a conflict. A group's permission no longer names its gid as user.id.

User, group and standalone permissions took their ids from the user or
group id, so a group's permission collided with the same-numbered user's.
get({ id }) skipped groups to avoid answering for the wrong one, which
left group permissions unreachable by id.

They now share one sequence kept as the permission file's last_id, as
nt_perm does in mysql. getGroup honours deleted, and a standalone
create refuses a second permission for one target.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Allocation races, legacy-ID retargeting, incomplete high-water tracking, and revoked-permission reads remain unresolved.

4 open issues
What changed in this PR

This PR introduces a shared permission-ID sequence for JSON and TOML stores, aligning allocation with MySQL.

Changes:

  • Routes user, group, and standalone permission allocation through one sequence.
  • Adds group permission lookup by ID, revoked-group filtering, and duplicate-owner checks.
  • Expands permission-ID and file-store regression tests.
File Description
lib/​user/​store/​file.js Uses shared allocation for explicit user permissions.
lib/​store/​file.test.js Updates fixtures and tests explicit-ID tracking.
lib/​permission/​test/​index.js Adds shared permission identity and revocation tests.
lib/​permission/​store/​file.js Implements shared allocation and group-ID access.
lib/​group/​store/​file.js Uses shared allocation for group permissions.

Review effort: Balanced

Comment thread lib/permission/store/file.js Outdated
Comment on lines +56 to +58
if (id === undefined) id = nextId(held, data.last_id)
else if (held.some((perm) => perm.id === id)) throw new StoreConflictError('permission', id)
data.last_id = Math.max(data.last_id ?? 0, id)
Comment on lines +242 to +245
const changedGroup = await this.groupFile.mutate('group', (groups) => {
const group = groups.find((g) => g.permissions?.id === id)
if (!group) return false
change(group, 'permissions')

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in 798f8cc. On stores written by main, a repeated destroy now returns false and leaves the group's permission alone

Comment thread lib/permission/store/file.js Outdated
held.push(...standalone)
if (id === undefined) id = nextId(held, data.last_id)
else if (held.some((perm) => perm.id === id)) throw new StoreConflictError('permission', id)
data.last_id = Math.max(data.last_id ?? 0, id)
Comment thread lib/permission/store/file.js Outdated
Comment on lines 200 to 201
const perm = this._postProcess(holder.permissions, deletedArg)
if (deletedArg === true && perm.deleted !== true) return undefined
Migration ran on the first id-based permission call, and only for a user
and group sharing an id. A group read handed out the old id, and a user
destroyed or inheriting first hid the duplicate. Main also left ids
repeated between standalone rows and owners, and between two users.

migrate() now runs once before the server takes requests, and the
permission file records that it has. It starts the sequence past the
owners' last_ids and the highest id held, then renumbers every repeat
among users and standalone rows. The row main's get({ id }) answered
with keeps the id, so ids clients hold still mean the same row. Every
group permission gets a new id: main never reached one by id, so the id
may be one a user's permission held before it was removed.

get({ uid }) and get({ gid }) filter revoked permissions the way
get({ id }) does, matching mysql. The race regression covers user create,
user going explicit and migration, with a racer that takes the group
path, since a user's racer waits on the user file lock.

Group.get and User.get drop a revoked permission as well, so a zone no
longer seeds nameservers from one, and Group.put leaves it alone. A
regrant takes a new id, as mysql's delete and insert does, so a stale
delete can't revoke it. A group's permission no longer names its gid as
a user.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants