openvibers/openvibe.network Commits

Network: restore Space's events grant (a revoked default grant came back only by name) (#161)

OP OpenVibers committed 12 Oct 2026 · f73a3a466e · 2 files, +47 −1

Network#160 made `['space', 'events.event.publish', 'openvibe.events']` a default grant again (Space publishes its static hosting events since T20 step 2). On production, Space's token for openvibe.events still answers `invalid_scope`. The row from the 10-08 revocation exists, and the boot seed inserts with `ON CONFLICT DO NOTHING`, so it stayed revoked.

- `RESTORED_GRANTS` names the default grants that code once revoked and that are defaults again. Boot clears `revoked_at` on exactly those rows (`granted_by = 'default'`). A grant someone revoked through grants-admin is never touched, the same rule as `CHANGED_DEFAULT_NAMESPACES`.
- First entry: Space's `events.event.publish`.
- `test/restored-grants.test.js`:
  - every restored grant is a default and not in REVOKED_GRANTS;
  - a revoked row of it is live again after boot;
  - any other revoked default grant stays revoked.

Tests: restored-grants, principals, service-principal.

Changes

ddiff --git a/server/identity/principals.js b/server/identity/principals.js
iindex 00760e42..60a981e3 100644
---- a/server/identity/principals.js
++++ b/server/identity/principals.js
@@@ -393,6 +393,14 @@ const REVOKED_GRANTS = [
// Default grants whose namespaces changed: a row still exactly as the old default seeded it is moved to
// the new list at boot, in this order (a row someone edited is left alone). Live's write grant lost
// chat.preferences when the namespace moved to Chat (Wave 6), then chat.tts_defaults (contracts 0.41.0).
++// Default grants that REVOKED_GRANTS once revoked and that are defaults again. The seed below inserts with ON CONFLICT
++// DO NOTHING, so a revoked row would otherwise stay revoked; only these named rows are restored (a grant a person revoked
++// through grants-admin is never touched). Each entry must also be in DEFAULT_GRANTS and no longer in REVOKED_GRANTS.
++const RESTORED_GRANTS = [
++ // T20 step 2 (2026-10-12): Space publishes its static hosting events again (revoked when the forum left, 10-08).
++ ['space', 'events.event.publish', 'openvibe.events'],
++];
++
const CHANGED_DEFAULT_NAMESPACES = [
['live', 'network.modules.write', SELF_AUDIENCE, ['chat.preferences', 'chat.tts_defaults', 'live.profile'], ['chat.tts_defaults', 'live.profile']],
['live', 'network.modules.write', SELF_AUDIENCE, ['chat.tts_defaults', 'live.profile'], ['live.profile', 'live.stats']],
@@@ -415,6 +423,9 @@ async function ensureSchema(db) {
for (const [client, cap, aud] of REVOKED_GRANTS) {
await db.prepare("UPDATE principal_grants SET revoked_at = ov_now() WHERE client_id = ? AND capability = ? AND audience = ? AND revoked_at IS NULL").run(client, cap, aud);
}
++ for (const [client, cap, aud] of RESTORED_GRANTS) {
++ await db.prepare("UPDATE principal_grants SET revoked_at = NULL WHERE client_id = ? AND capability = ? AND audience = ? AND granted_by = 'default' AND revoked_at IS NOT NULL").run(client, cap, aud);
++ }
for (const [client, cap, aud, was, now] of CHANGED_DEFAULT_NAMESPACES) {
await db.prepare("UPDATE principal_grants SET namespaces = ? WHERE client_id = ? AND capability = ? AND audience = ? AND granted_by = 'default' AND namespaces = ?")
.run(JSON.stringify(now), client, cap, aud, JSON.stringify(was));
@@@ -562,4 +573,4 @@ function guard(capability, { ownApp, namespace } = {}) {
};
}
--module.exports = { ensureSchema, issueToken, guard, grantsFor, recordDecision, DEFAULT_GRANTS, REVOKED_GRANTS, TOKEN_TTL_S, SELF_AUDIENCE };
++module.exports = { ensureSchema, issueToken, guard, grantsFor, recordDecision, DEFAULT_GRANTS, REVOKED_GRANTS, RESTORED_GRANTS, TOKEN_TTL_S, SELF_AUDIENCE };
ddiff --git a/test/restored-grants.test.js b/test/restored-grants.test.js
nnew file mode 100644
iindex 00000000..a73a28b3
---- /dev/null
++++ b/test/restored-grants.test.js
@@@ -0,0 +1,35 @@
++'use strict';
++// A default grant that code revoked (REVOKED_GRANTS) and later made a default again comes back at boot only when it is
++// named in RESTORED_GRANTS; a grant a person revoked stays revoked. (2026-10-12: Space's events.event.publish stayed
++// revoked after Network#160 because the seed inserts with ON CONFLICT DO NOTHING.)
++const assert = require('assert');
++const { getDb } = require('../server/db/database');
++const principals = require('../server/identity/principals');
++
++(async () => {
++ const db = await getDb();
++ await principals.ensureSchema(db);
++ const key = (g) => g.slice(0, 3).join(' ');
++ const defaults = new Set(principals.DEFAULT_GRANTS.map(key));
++ const revoked = new Set(principals.REVOKED_GRANTS.map(key));
++ for (const g of principals.RESTORED_GRANTS) {
++ assert.ok(defaults.has(key(g)), `${key(g)} is restored, so it must be a default grant`);
++ assert.ok(!revoked.has(key(g)), `${key(g)} is restored, so it must not be revoked`);
++ }
++ const row = (c, cap, aud) => db.prepare('SELECT revoked_at FROM principal_grants WHERE client_id = ? AND capability = ? AND audience = ?').get(c, cap, aud);
++ const [client, cap, aud] = principals.RESTORED_GRANTS[0];
++ assert.ok(await db.prepare('SELECT 1 FROM oauth_clients WHERE client_id = ?').get(client), `the ${client} client is seeded`);
++ {
++ await db.prepare('UPDATE principal_grants SET revoked_at = ov_now() WHERE client_id = ? AND capability = ? AND audience = ?').run(client, cap, aud);
++ await principals.ensureSchema(db);
++ assert.strictEqual((await row(client, cap, aud)).revoked_at, null, 'the restored default grant is live again after boot');
++ }
++ // A default grant that is not in RESTORED_GRANTS and was revoked (by a person, through grants-admin) stays revoked.
++ const other = principals.DEFAULT_GRANTS.find((g) => !principals.RESTORED_GRANTS.some((r) => key(r) === key(g)) && g[0] === 'host');
++ await db.prepare('UPDATE principal_grants SET revoked_at = ov_now() WHERE client_id = ? AND capability = ? AND audience = ?').run(other[0], other[1], other[2]);
++ await principals.ensureSchema(db);
++ const kept = await row(other[0], other[1], other[2]);
++ assert.ok(!kept || kept.revoked_at, 'a grant someone revoked is never restored by boot');
++ console.log('restored grants: a named default grant comes back; any other revoked grant stays revoked');
++ process.exit(0);
++})().catch((err) => { console.error(err); process.exit(1); });