openvibers/openvibe.host Commits
Staff powers from the contracts staff map (WS-D, ADR-022; Contracts 0.38.0): Host staff (quotas, takedowns, maintaining any project) are staff.site.configure (admin, as before), not a role list. test/staff-map.test.js fails on raw role checks
Changes
@@@ -16,7 +16,7 @@"dotenv": "^16.4.7","express": "^4.21.2","express-rate-limit": "^7.5.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",++ "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"},@@@ -1412,9 +1412,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.33.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",-- "integrity": "sha512-7q8NUhcPxgb8lxrVdC3pFbiPdjzliJM1mMP/1G5a2PmOr2tJVAXwYa8+a8YgCJvYeMSeMHWijLkIEiZ9IhllOg==",++ "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",@@@ -31,7 +31,7 @@"dotenv": "^16.4.7","express": "^4.21.2","express-rate-limit": "^7.5.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",++ "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"},@@@ -7,7 +7,8 @@* { kind: 'user', subject: 'usr_…', staff, user }* API: a Network user JWT in `Authorization: Bearer` (cookies are ignored on /api/v1).* Dashboard: the __Host- session cookie (never the navbar's ov_token, which a tenant-- * subdomain could plant). role 'admin' makes the viewer staff.++ * subdomain could plant). staff = the contracts staff map's staff.site.configure (ADR-022):++ * quotas, takedowns, and maintaining or deleting any project (never deploying to it).* { kind: 'service', service: 'svc:codes' | 'app:app_…', claims, subject, env }* A Network client-credentials token for audience openvibe.host. A first-party service* (svc:) may name the person it acts for with X-OV-Subject; an app (app:) always acts as@@@ -21,8 +22,7 @@ const contracts = require('openvibe-contracts');const { sessionToken, bearerToken, claimsToUser, decodeJwtPayload } = require('./sso');const { checkCapability } = require('./capabilities');--const { ids, serviceAuth, http } = contracts;--const STAFF_ROLES = new Set(['admin']);++const { ids, serviceAuth, http, staff: staffMap } = contracts;const PRINCIPAL_SUB = /^(svc|app|mod):/;const AUDIENCE = 'openvibe.host';@@@ -64,7 +64,7 @@ function createViewerResolver({ auth, config }) {if (claims.typ !== undefined || claims.nonce !== undefined) return null;const subject = ids.isSubjectId('user', claims.subject_id) ? claims.subject_id : null;if (!subject) return null;-- return { kind: 'user', subject, staff: STAFF_ROLES.has(claims.role), user: claimsToUser(claims) };++ return { kind: 'user', subject, staff: staffMap.can(claims, 'staff.site.configure'), user: claimsToUser(claims) };}async function resolveApi(req) {@@@ -112,4 +112,4 @@ function guard(cap) {};}--module.exports = { createViewerResolver, guard, ANONYMOUS, ViewerError, STAFF_ROLES, AUDIENCE };++module.exports = { createViewerResolver, guard, ANONYMOUS, ViewerError, AUDIENCE };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 Host staff (quotas, takedowns, maintaining any project).++// 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 decisions (display reads, an entity's own roles), allowed by file and snippet.++const ALLOW = [];++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');
@@ -16,7 +16,7 @@
@@ -16,7 +16,7 @@
"dotenv": "^16.4.7",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
+ "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"
},
},
@@ -1412,9 +1412,9 @@
@@ -1412,9 +1412,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.33.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
- "integrity": "sha512-7q8NUhcPxgb8lxrVdC3pFbiPdjzliJM1mMP/1G5a2PmOr2tJVAXwYa8+a8YgCJvYeMSeMHWijLkIEiZ9IhllOg==",
+ "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",
@@ -31,7 +31,7 @@
@@ -31,7 +31,7 @@
"dotenv": "^16.4.7",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
+ "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"
},
},
@@ -7,7 +7,8 @@
@@ -7,7 +7,8 @@
* { kind: 'user', subject: 'usr_…', staff, user }
* { kind: 'user', subject: 'usr_…', staff, user }
* API: a Network user JWT in `Authorization: Bearer` (cookies are ignored on /api/v1).
* API: a Network user JWT in `Authorization: Bearer` (cookies are ignored on /api/v1).
* Dashboard: the __Host- session cookie (never the navbar's ov_token, which a tenant
* Dashboard: the __Host- session cookie (never the navbar's ov_token, which a tenant
- * subdomain could plant). role 'admin' makes the viewer staff.
+ * subdomain could plant). staff = the contracts staff map's staff.site.configure (ADR-022):
+ * quotas, takedowns, and maintaining or deleting any project (never deploying to it).
* { kind: 'service', service: 'svc:codes' | 'app:app_…', claims, subject, env }
* { kind: 'service', service: 'svc:codes' | 'app:app_…', claims, subject, env }
* A Network client-credentials token for audience openvibe.host. A first-party service
* A Network client-credentials token for audience openvibe.host. A first-party service
* (svc:) may name the person it acts for with X-OV-Subject; an app (app:) always acts as
* (svc:) may name the person it acts for with X-OV-Subject; an app (app:) always acts as
@@ -21,8 +22,7 @@ const contracts = require('openvibe-contracts');
@@ -21,8 +22,7 @@ const contracts = require('openvibe-contracts');
const { sessionToken, bearerToken, claimsToUser, decodeJwtPayload } = require('./sso');
const { sessionToken, bearerToken, claimsToUser, decodeJwtPayload } = require('./sso');
const { checkCapability } = require('./capabilities');
const { checkCapability } = require('./capabilities');
-const { ids, serviceAuth, http } = contracts;
-const STAFF_ROLES = new Set(['admin']);
+const { ids, serviceAuth, http, staff: staffMap } = contracts;
const PRINCIPAL_SUB = /^(svc|app|mod):/;
const PRINCIPAL_SUB = /^(svc|app|mod):/;
const AUDIENCE = 'openvibe.host';
const AUDIENCE = 'openvibe.host';
@@ -64,7 +64,7 @@ function createViewerResolver({ auth, config }) {
@@ -64,7 +64,7 @@ function createViewerResolver({ auth, config }) {
if (claims.typ !== undefined || claims.nonce !== undefined) return null;
if (claims.typ !== undefined || claims.nonce !== undefined) return null;
const subject = ids.isSubjectId('user', claims.subject_id) ? claims.subject_id : null;
const subject = ids.isSubjectId('user', claims.subject_id) ? claims.subject_id : null;
if (!subject) return null;
if (!subject) return null;
- return { kind: 'user', subject, staff: STAFF_ROLES.has(claims.role), user: claimsToUser(claims) };
+ return { kind: 'user', subject, staff: staffMap.can(claims, 'staff.site.configure'), user: claimsToUser(claims) };
}
}
async function resolveApi(req) {
async function resolveApi(req) {
@@ -112,4 +112,4 @@ function guard(cap) {
@@ -112,4 +112,4 @@ function guard(cap) {
};
};
}
}
-module.exports = { createViewerResolver, guard, ANONYMOUS, ViewerError, STAFF_ROLES, AUDIENCE };
+module.exports = { createViewerResolver, guard, ANONYMOUS, ViewerError, AUDIENCE };
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 Host staff (quotas, takedowns, maintaining any project).
+// 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 decisions (display reads, an entity's own roles), allowed by file and snippet.
+const ALLOW = [];
+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');