openvibers/openvibe.network Commits
Network issues staff capabilities in user tokens (D05, ADR-022): staff members' access tokens (login and OAuth) carry staff_caps from the contracts staff map, and the owner carries is_owner; people who are not staff carry neither, so their tokens stay small. Services' staff.can() prefers issued claims, so a map change reaches every service with the next token. test/staff-claims.test.js
Changes
@@@ -9,6 +9,7 @@ const express = require('express');const crypto = require('crypto');const jwt = require('jsonwebtoken');const subjects = require('../identity/subjects');++const { staffClaims } = require('./staff-claims');const principals = require('../identity/principals');const devTokens = require('../developer/tokens');const oidc = require('./oidc');@@@ -397,6 +398,7 @@ function issueTokenPair(db, config, req, user, client, family = {}) {role: user.role,avatar_url: user.avatar_url,profile_color: user.profile_color,++ ...staffClaims(user),},privateKey,{@@@ -11,6 +11,7 @@ const crypto = require('crypto');const jwt = require('jsonwebtoken');const { v4: uuidv4 } = require('uuid');const subjects = require('../identity/subjects');++const { staffClaims } = require('./staff-claims');const router = express.Router();@@@ -62,6 +63,7 @@ function signToken(user, privateKey, config) {role: user.role,avatar_url: user.avatar_url,profile_color: user.profile_color,++ ...staffClaims(user),},privateKey,{nnew file mode 100644@@@ -0,0 +1,20 @@++'use strict';++/**++ * Staff claims in user tokens (ADR-022, openvibe-contracts manifests/policy/staff-roles.json): Network owns++ * the roles and issues each staff member's capabilities as `staff_caps`, and the owner as `is_owner: true`.++ * Services ask `staff.can(claims, 'staff.<area>.<action>')`; issued claims win over the role, so a change++ * to the map reaches every service with the next token. People who are not staff carry neither claim++ * (the map gives them nothing either way), which keeps their tokens small.++ */++const { staff } = require('openvibe-contracts');++const { isOwner } = require('./owner-guard');++++function staffClaims(user) {++ if (!user) return {};++ const owner = user.role === 'admin' && isOwner(user);++ const caps = staff.capabilitiesOf({ role: user.role, is_owner: owner });++ if (!caps.length) return {};++ return { ...(owner ? { is_owner: true } : {}), staff_caps: caps };++}++++module.exports = { staffClaims };nnew file mode 100644@@@ -0,0 +1,22 @@++'use strict';++/** Network issues each staff member's capabilities (staff_caps) and the owner flag in user tokens (ADR-022). */++const assert = require('assert');++process.env.OWNER_USERNAME = 'theowner';++const { staff } = require('openvibe-contracts');++const { staffClaims } = require('../server/auth/staff-claims');++++assert.deepStrictEqual(staffClaims({ role: 'user', username: 'a' }), {}, 'not staff: no claims, small token');++assert.deepStrictEqual(staffClaims({ role: 'streamer', username: 'b' }), {});++assert.deepStrictEqual(staffClaims({ role: 'global_mod', username: 'm' }), { staff_caps: staff.capabilitiesOf('global_mod') });++assert.deepStrictEqual(staffClaims({ role: 'admin', username: 'boss' }), { staff_caps: staff.capabilitiesOf('admin') });++const owner = staffClaims({ role: 'admin', username: 'TheOwner' });++assert.strictEqual(owner.is_owner, true);++assert.deepStrictEqual(owner.staff_caps, staff.capabilitiesOf('owner'));++assert.deepStrictEqual(staffClaims({ role: 'user', username: 'theowner' }), {}, 'the owner name without the admin role is nobody');++// Services read the issued claims exactly as the map answers.++assert.strictEqual(staff.can(owner, 'staff.secrets.manage'), true);++assert.strictEqual(staff.can({ role: 'admin', ...staffClaims({ role: 'admin', username: 'boss' }) }, 'staff.secrets.manage'), false);++// Both user-token signers add them.++const fs = require('fs');++for (const f of ['server/auth/routes.js', 'server/auth/oauth-routes.js']) assert.ok(fs.readFileSync(require('path').join(__dirname, '..', f), 'utf8').includes('...staffClaims(user)'), f);++console.log('staff claims: all checks passed');
@@ -9,6 +9,7 @@ const express = require('express');
@@ -9,6 +9,7 @@ const express = require('express');
const crypto = require('crypto');
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const jwt = require('jsonwebtoken');
const subjects = require('../identity/subjects');
const subjects = require('../identity/subjects');
+const { staffClaims } = require('./staff-claims');
const principals = require('../identity/principals');
const principals = require('../identity/principals');
const devTokens = require('../developer/tokens');
const devTokens = require('../developer/tokens');
const oidc = require('./oidc');
const oidc = require('./oidc');
@@ -397,6 +398,7 @@ function issueTokenPair(db, config, req, user, client, family = {}) {
@@ -397,6 +398,7 @@ function issueTokenPair(db, config, req, user, client, family = {}) {
role: user.role,
role: user.role,
avatar_url: user.avatar_url,
avatar_url: user.avatar_url,
profile_color: user.profile_color,
profile_color: user.profile_color,
+ ...staffClaims(user),
},
},
privateKey,
privateKey,
{
{
@@ -11,6 +11,7 @@ const crypto = require('crypto');
@@ -11,6 +11,7 @@ const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const jwt = require('jsonwebtoken');
const { v4: uuidv4 } = require('uuid');
const { v4: uuidv4 } = require('uuid');
const subjects = require('../identity/subjects');
const subjects = require('../identity/subjects');
+const { staffClaims } = require('./staff-claims');
const router = express.Router();
const router = express.Router();
@@ -62,6 +63,7 @@ function signToken(user, privateKey, config) {
@@ -62,6 +63,7 @@ function signToken(user, privateKey, config) {
role: user.role,
role: user.role,
avatar_url: user.avatar_url,
avatar_url: user.avatar_url,
profile_color: user.profile_color,
profile_color: user.profile_color,
+ ...staffClaims(user),
},
},
privateKey,
privateKey,
{
{
new file mode 100644
new file mode 100644
@@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
+'use strict';
+/**
+ * Staff claims in user tokens (ADR-022, openvibe-contracts manifests/policy/staff-roles.json): Network owns
+ * the roles and issues each staff member's capabilities as `staff_caps`, and the owner as `is_owner: true`.
+ * Services ask `staff.can(claims, 'staff.<area>.<action>')`; issued claims win over the role, so a change
+ * to the map reaches every service with the next token. People who are not staff carry neither claim
+ * (the map gives them nothing either way), which keeps their tokens small.
+ */
+const { staff } = require('openvibe-contracts');
+const { isOwner } = require('./owner-guard');
+
+function staffClaims(user) {
+ if (!user) return {};
+ const owner = user.role === 'admin' && isOwner(user);
+ const caps = staff.capabilitiesOf({ role: user.role, is_owner: owner });
+ if (!caps.length) return {};
+ return { ...(owner ? { is_owner: true } : {}), staff_caps: caps };
+}
+
+module.exports = { staffClaims };
new file mode 100644
new file mode 100644
@@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
+'use strict';
+/** Network issues each staff member's capabilities (staff_caps) and the owner flag in user tokens (ADR-022). */
+const assert = require('assert');
+process.env.OWNER_USERNAME = 'theowner';
+const { staff } = require('openvibe-contracts');
+const { staffClaims } = require('../server/auth/staff-claims');
+
+assert.deepStrictEqual(staffClaims({ role: 'user', username: 'a' }), {}, 'not staff: no claims, small token');
+assert.deepStrictEqual(staffClaims({ role: 'streamer', username: 'b' }), {});
+assert.deepStrictEqual(staffClaims({ role: 'global_mod', username: 'm' }), { staff_caps: staff.capabilitiesOf('global_mod') });
+assert.deepStrictEqual(staffClaims({ role: 'admin', username: 'boss' }), { staff_caps: staff.capabilitiesOf('admin') });
+const owner = staffClaims({ role: 'admin', username: 'TheOwner' });
+assert.strictEqual(owner.is_owner, true);
+assert.deepStrictEqual(owner.staff_caps, staff.capabilitiesOf('owner'));
+assert.deepStrictEqual(staffClaims({ role: 'user', username: 'theowner' }), {}, 'the owner name without the admin role is nobody');
+// Services read the issued claims exactly as the map answers.
+assert.strictEqual(staff.can(owner, 'staff.secrets.manage'), true);
+assert.strictEqual(staff.can({ role: 'admin', ...staffClaims({ role: 'admin', username: 'boss' }) }, 'staff.secrets.manage'), false);
+// Both user-token signers add them.
+const fs = require('fs');
+for (const f of ['server/auth/routes.js', 'server/auth/oauth-routes.js']) assert.ok(fs.readFileSync(require('path').join(__dirname, '..', f), 'utf8').includes('...staffClaims(user)'), f);
+console.log('staff claims: all checks passed');