openvibers/openvibe.codes Commits
Staff powers from the contracts staff map (WS-D, ADR-022; Contracts 0.38.0, docs regenerate from it): Codes staff are staff.site.configure (admin, as before) or CODES_STAFF_SUBJECTS (break-glass), not a role comparison. test/staff-map.test.js fails on raw role checks (project roles and display reads allowed by name)
Changes
@@@ -20,7 +20,8 @@ OV_OAUTH_CLIENT_SECRET=CODES_FORM_SECRET=COOKIE_SECURE=true--# Codes staff (trust tiers, metadata only): Network admins, plus these usr_ subjects (comma list).++# Codes staff (trust tiers, metadata only): staff with staff.site.configure (Network admins), plus these++# usr_ subjects (comma list; the break-glass way in).CODES_STAFF_SUBJECTS=# Playgrounds call these with the APP's own token (defaults: the public origins in openvibe-contracts).@@@ -16,7 +16,7 @@"express": "^4.21.2","express-rate-limit": "^7.5.0","helmet": "^8.1.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.34.2",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.38.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.12.0"},@@@ -1023,9 +1023,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.34.2",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.34.2",-- "integrity": "sha512-LV+8rnyfok0c7JsWymmrmo49BGL+nFvvcB75l1Ogx5iVoTw6PmX+jFUbQtnvYchKPmnzu2irfT2VOI49GSro7w==",++ "version": "0.38.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.38.0",++ "integrity": "sha512-nHYCcKM9N4TJS+BlHOax9xwt4jU87qhXsLZ2gnuK4nOHpGlkLk2OI49PCQVvfHPe+V7bzBK7L4MKjFcTNv8ORA==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -27,7 +27,7 @@"express": "^4.21.2","express-rate-limit": "^7.5.0","helmet": "^8.1.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.34.2",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.38.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.12.0"},@@@ -1,4 +1,5 @@'use strict';++const { staff: staffMap } = require('openvibe-contracts');/*** Sign-in with OpenVibe.Network: OAuth 2 authorization code with PKCE (S256), as OAuth client@@@ -106,7 +107,8 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Datusername: typeof claims.username === 'string' ? claims.username.slice(0, 64) : null,displayName: typeof claims.display_name === 'string' ? claims.display_name.slice(0, 80) : (typeof claims.username === 'string' ? claims.username.slice(0, 64) : 'you'),role,-- staff: role === 'admin' || (subject && config.staffSubjects.includes(subject)),++ // Staff = the contracts staff map's staff.site.configure (ADR-022), or a subject in CODES_STAFF_SUBJECTS.++ staff: staffMap.can(claims, 'staff.site.configure') || Boolean(subject && config.staffSubjects.includes(subject)),// The person's Network access token: used for server-side Network calls, never rendered.token,};@@@ -55,7 +55,7 @@ function load(env = process.env) {// Signs the per-person form token (CSRF). Unset: a random per-process key.formSecret: env.CODES_FORM_SECRET || '',-- // Staff (trust tiers): Network admins, plus these subjects (usr_…).++ // Staff (trust tiers): staff.site.configure (Network admins), plus these subjects (usr_…).staffSubjects: list(env.CODES_STAFF_SUBJECTS),// Where playgrounds call, with the APP's own token (never Codes' credentials). Defaults are@@@ -19,11 +19,11 @@ const sdkPkg = require('openvibe-sdk/package.json');await check('package.json pins the contracts, SDK and shared tags that are installed', async () => {const tag = (dep) => (pkg.dependencies[dep].match(/refs\/tags\/(v[0-9.]+)$/) || [])[1];-- assert.strictEqual(tag('openvibe-contracts'), 'v0.34.2');++ assert.strictEqual(tag('openvibe-contracts'), 'v0.38.0');assert.strictEqual(tag('openvibe-sdk'), `v${sdkPkg.version}`);assert.strictEqual(tag('openvibe-shared'), `v${require('openvibe-shared/package.json').version}`);assert.strictEqual(sdkPkg.version, '0.5.0');-- // The contracts tag v0.34.2 carries the released codes manifest and codes.app-manifest@1.++ // The contracts tag v0.38.0 carries the released codes manifest and codes.app-manifest@1.assert.ok(contracts.services.get('codes') && contracts.services.get('codes').status === 'alpha');assert.ok(contracts.resolve('codes.app-manifest@1'));});@@@ -33,13 +33,13 @@ const sdkPkg = require('openvibe-sdk/package.json');const r = await t.get(p);assert.strictEqual(r.status, 200, p);assert.ok(r.text.includes(`openvibe-contracts v${contractsVersion}`), `${p} names the installed contracts version`);-- assert.ok(r.text.includes('OpenVibe.Contracts/tree/v0.34.2'), `${p} links the pinned tag`);-- if (contractsVersion !== '0.34.2') assert.ok(r.text.includes('(tag v0.34.2)'), `${p} says the tag differs from the package version`);++ assert.ok(r.text.includes('OpenVibe.Contracts/tree/v0.38.0'), `${p} links the pinned tag`);++ if (contractsVersion !== '0.38.0') assert.ok(r.text.includes('(tag v0.38.0)'), `${p} says the tag differs from the package version`);assert.ok(r.text.includes(`openvibe-sdk v${sdkPkg.version}`), `${p} names the SDK version`);}const v = (await t.get('/api/v1/docs/versions')).json();assert.strictEqual(v.contracts, contractsVersion);-- assert.strictEqual(v.contracts_tag, 'v0.34.2');++ assert.strictEqual(v.contracts_tag, 'v0.38.0');assert.strictEqual(v.sdk, sdkPkg.version);});nnew file mode 100644@@@ -0,0 +1,28 @@++'use strict';++// Staff powers come from the openvibe-contracts staff map (ADR-022): staff.site.configure makes a person Codes staff (CODES_STAFF_SUBJECTS names more).++// No server file compares a person's role by hand.++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const { staff } = require('openvibe-contracts');++++const CAPS = ["staff.site.configure"];++for (const [claims, want] of [[{"role": "user"}, [false]], [{"role": "global_mod"}, [false]], [{"role": "admin"}, [true]], [{"role": "admin", "is_owner": true}, [true]]]) {++ CAPS.forEach((cap, i) => assert.strictEqual(staff.can(claims, cap), want[i], `${JSON.stringify(claims)} ${cap}`));++}++++// Not staff: reading the role claim for display, and a project's own roles (owner/admin/developer/viewer).++const ALLOW = [['server/auth/sso.js', "typeof claims.role === 'string'"], ['server/http/portal.js', "const assignable = role === 'owner'"]];++const offenders = [];++(function walk(dir) {++ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {++ const f = path.join(dir, e.name);++ if (e.isDirectory()) { if (e.name !== 'node_modules') walk(f); continue; }++ if (!/\.(js|ts)$/.test(e.name)) continue;++ fs.readFileSync(f, 'utf8').split('\n').forEach((line, i) => {++ if (/STAFF_ROLES|claims\.role\s*[!=]==?|\.(has|includes)\(\s*(claims|u|user|p)\.role\b|\brole\s*[!=]==?\s*['"](admin|global_mod|moderator)['"]/.test(line)) { const rel = path.relative(path.join(__dirname, '..'), f).split(path.sep).join('/'); if (!ALLOW.some(([file, snip]) => file === rel && line.includes(snip))) offenders.push(`${rel}:${i + 1}`); }++ });++ }++})(path.join(__dirname, '..', "server"));++assert.deepStrictEqual(offenders, [], 'raw role checks; ask staff.can(claims, \'staff.…\')');++console.log('staff map: all checks passed');
@@ -20,7 +20,8 @@ OV_OAUTH_CLIENT_SECRET=
@@ -20,7 +20,8 @@ OV_OAUTH_CLIENT_SECRET=
CODES_FORM_SECRET=
CODES_FORM_SECRET=
COOKIE_SECURE=true
COOKIE_SECURE=true
-# Codes staff (trust tiers, metadata only): Network admins, plus these usr_ subjects (comma list).
+# Codes staff (trust tiers, metadata only): staff with staff.site.configure (Network admins), plus these
+# usr_ subjects (comma list; the break-glass way in).
CODES_STAFF_SUBJECTS=
CODES_STAFF_SUBJECTS=
# Playgrounds call these with the APP's own token (defaults: the public origins in openvibe-contracts).
# Playgrounds call these with the APP's own token (defaults: the public origins in openvibe-contracts).
@@ -16,7 +16,7 @@
@@ -16,7 +16,7 @@
"express": "^4.21.2",
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.34.2",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.38.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.12.0"
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.12.0"
},
},
@@ -1023,9 +1023,9 @@
@@ -1023,9 +1023,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.34.2",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.34.2",
- "integrity": "sha512-LV+8rnyfok0c7JsWymmrmo49BGL+nFvvcB75l1Ogx5iVoTw6PmX+jFUbQtnvYchKPmnzu2irfT2VOI49GSro7w==",
+ "version": "0.38.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.38.0",
+ "integrity": "sha512-nHYCcKM9N4TJS+BlHOax9xwt4jU87qhXsLZ2gnuK4nOHpGlkLk2OI49PCQVvfHPe+V7bzBK7L4MKjFcTNv8ORA==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -27,7 +27,7 @@
@@ -27,7 +27,7 @@
"express": "^4.21.2",
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.34.2",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.38.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.12.0"
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.12.0"
},
},
@@ -1,4 +1,5 @@
@@ -1,4 +1,5 @@
'use strict';
'use strict';
+const { staff: staffMap } = require('openvibe-contracts');
/**
/**
* Sign-in with OpenVibe.Network: OAuth 2 authorization code with PKCE (S256), as OAuth client
* Sign-in with OpenVibe.Network: OAuth 2 authorization code with PKCE (S256), as OAuth client
@@ -106,7 +107,8 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Dat
@@ -106,7 +107,8 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Dat
username: typeof claims.username === 'string' ? claims.username.slice(0, 64) : null,
username: typeof claims.username === 'string' ? claims.username.slice(0, 64) : null,
displayName: typeof claims.display_name === 'string' ? claims.display_name.slice(0, 80) : (typeof claims.username === 'string' ? claims.username.slice(0, 64) : 'you'),
displayName: typeof claims.display_name === 'string' ? claims.display_name.slice(0, 80) : (typeof claims.username === 'string' ? claims.username.slice(0, 64) : 'you'),
role,
role,
- staff: role === 'admin' || (subject && config.staffSubjects.includes(subject)),
+ // Staff = the contracts staff map's staff.site.configure (ADR-022), or a subject in CODES_STAFF_SUBJECTS.
+ staff: staffMap.can(claims, 'staff.site.configure') || Boolean(subject && config.staffSubjects.includes(subject)),
// The person's Network access token: used for server-side Network calls, never rendered.
// The person's Network access token: used for server-side Network calls, never rendered.
token,
token,
};
};
@@ -55,7 +55,7 @@ function load(env = process.env) {
@@ -55,7 +55,7 @@ function load(env = process.env) {
// Signs the per-person form token (CSRF). Unset: a random per-process key.
// Signs the per-person form token (CSRF). Unset: a random per-process key.
formSecret: env.CODES_FORM_SECRET || '',
formSecret: env.CODES_FORM_SECRET || '',
- // Staff (trust tiers): Network admins, plus these subjects (usr_…).
+ // Staff (trust tiers): staff.site.configure (Network admins), plus these subjects (usr_…).
staffSubjects: list(env.CODES_STAFF_SUBJECTS),
staffSubjects: list(env.CODES_STAFF_SUBJECTS),
// Where playgrounds call, with the APP's own token (never Codes' credentials). Defaults are
// Where playgrounds call, with the APP's own token (never Codes' credentials). Defaults are
@@ -19,11 +19,11 @@ const sdkPkg = require('openvibe-sdk/package.json');
@@ -19,11 +19,11 @@ const sdkPkg = require('openvibe-sdk/package.json');
await check('package.json pins the contracts, SDK and shared tags that are installed', async () => {
await check('package.json pins the contracts, SDK and shared tags that are installed', async () => {
const tag = (dep) => (pkg.dependencies[dep].match(/refs\/tags\/(v[0-9.]+)$/) || [])[1];
const tag = (dep) => (pkg.dependencies[dep].match(/refs\/tags\/(v[0-9.]+)$/) || [])[1];
- assert.strictEqual(tag('openvibe-contracts'), 'v0.34.2');
+ assert.strictEqual(tag('openvibe-contracts'), 'v0.38.0');
assert.strictEqual(tag('openvibe-sdk'), `v${sdkPkg.version}`);
assert.strictEqual(tag('openvibe-sdk'), `v${sdkPkg.version}`);
assert.strictEqual(tag('openvibe-shared'), `v${require('openvibe-shared/package.json').version}`);
assert.strictEqual(tag('openvibe-shared'), `v${require('openvibe-shared/package.json').version}`);
assert.strictEqual(sdkPkg.version, '0.5.0');
assert.strictEqual(sdkPkg.version, '0.5.0');
- // The contracts tag v0.34.2 carries the released codes manifest and codes.app-manifest@1.
+ // The contracts tag v0.38.0 carries the released codes manifest and codes.app-manifest@1.
assert.ok(contracts.services.get('codes') && contracts.services.get('codes').status === 'alpha');
assert.ok(contracts.services.get('codes') && contracts.services.get('codes').status === 'alpha');
assert.ok(contracts.resolve('codes.app-manifest@1'));
assert.ok(contracts.resolve('codes.app-manifest@1'));
});
});
@@ -33,13 +33,13 @@ const sdkPkg = require('openvibe-sdk/package.json');
@@ -33,13 +33,13 @@ const sdkPkg = require('openvibe-sdk/package.json');
const r = await t.get(p);
const r = await t.get(p);
assert.strictEqual(r.status, 200, p);
assert.strictEqual(r.status, 200, p);
assert.ok(r.text.includes(`openvibe-contracts v${contractsVersion}`), `${p} names the installed contracts version`);
assert.ok(r.text.includes(`openvibe-contracts v${contractsVersion}`), `${p} names the installed contracts version`);
- assert.ok(r.text.includes('OpenVibe.Contracts/tree/v0.34.2'), `${p} links the pinned tag`);
- if (contractsVersion !== '0.34.2') assert.ok(r.text.includes('(tag v0.34.2)'), `${p} says the tag differs from the package version`);
+ assert.ok(r.text.includes('OpenVibe.Contracts/tree/v0.38.0'), `${p} links the pinned tag`);
+ if (contractsVersion !== '0.38.0') assert.ok(r.text.includes('(tag v0.38.0)'), `${p} says the tag differs from the package version`);
assert.ok(r.text.includes(`openvibe-sdk v${sdkPkg.version}`), `${p} names the SDK version`);
assert.ok(r.text.includes(`openvibe-sdk v${sdkPkg.version}`), `${p} names the SDK version`);
}
}
const v = (await t.get('/api/v1/docs/versions')).json();
const v = (await t.get('/api/v1/docs/versions')).json();
assert.strictEqual(v.contracts, contractsVersion);
assert.strictEqual(v.contracts, contractsVersion);
- assert.strictEqual(v.contracts_tag, 'v0.34.2');
+ assert.strictEqual(v.contracts_tag, 'v0.38.0');
assert.strictEqual(v.sdk, sdkPkg.version);
assert.strictEqual(v.sdk, sdkPkg.version);
});
});
new file mode 100644
new file mode 100644
@@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
+'use strict';
+// Staff powers come from the openvibe-contracts staff map (ADR-022): staff.site.configure makes a person Codes staff (CODES_STAFF_SUBJECTS names more).
+// No server file compares a person's role by hand.
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const { staff } = require('openvibe-contracts');
+
+const CAPS = ["staff.site.configure"];
+for (const [claims, want] of [[{"role": "user"}, [false]], [{"role": "global_mod"}, [false]], [{"role": "admin"}, [true]], [{"role": "admin", "is_owner": true}, [true]]]) {
+ CAPS.forEach((cap, i) => assert.strictEqual(staff.can(claims, cap), want[i], `${JSON.stringify(claims)} ${cap}`));
+}
+
+// Not staff: reading the role claim for display, and a project's own roles (owner/admin/developer/viewer).
+const ALLOW = [['server/auth/sso.js', "typeof claims.role === 'string'"], ['server/http/portal.js', "const assignable = role === 'owner'"]];
+const offenders = [];
+(function walk(dir) {
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
+ const f = path.join(dir, e.name);
+ if (e.isDirectory()) { if (e.name !== 'node_modules') walk(f); continue; }
+ if (!/\.(js|ts)$/.test(e.name)) continue;
+ fs.readFileSync(f, 'utf8').split('\n').forEach((line, i) => {
+ if (/STAFF_ROLES|claims\.role\s*[!=]==?|\.(has|includes)\(\s*(claims|u|user|p)\.role\b|\brole\s*[!=]==?\s*['"](admin|global_mod|moderator)['"]/.test(line)) { const rel = path.relative(path.join(__dirname, '..'), f).split(path.sep).join('/'); if (!ALLOW.some(([file, snip]) => file === rel && line.includes(snip))) offenders.push(`${rel}:${i + 1}`); }
+ });
+ }
+})(path.join(__dirname, '..', "server"));
+assert.deepStrictEqual(offenders, [], 'raw role checks; ask staff.can(claims, \'staff.…\')');
+console.log('staff map: all checks passed');