openvibers/openvibe.bot Commits
Elements: a control service asks a person's role on a robot (#404)
Changes
@@@ -22,6 +22,7 @@* | DELETE /robots/:id | bot.robot.manage | owner |* | POST /robots/:id/pairing-code | bot.robot.manage | owner |* | GET|POST /robots/:id/operators | bot.robot.read/manage | owner (write) / member (read) |++ * | GET /robots/:id/role | bot.robot.control | a service asking about a subject, or a person asking about themself |* | DELETE /robots/:id/operators/:subject | bot.robot.manage | owner |* | GET /robots/:id/devices | bot.robot.read | a member |* | POST /devices/:id/rotate, /revoke | bot.device.connect | the robot's owner |@@@ -383,6 +384,22 @@ function v1Router({ domain, apiAuth, limits, hub, jobs: jobService, config, submawait member(req, req.params.id, ['owner', 'operator']);res.json({ operators: await domain.members.list(req.params.id) });}));++ // Elements stage 1: the service that may control a robot (Live, sending a deck press as the viewer)++ // asks which role that viewer holds on it. It is the same capability that lets it send the command, and++ // the answer only says what Bot's own gate would already tell it. A person may ask about themself only.++ r.get('/robots/:id/role', wrap(async (req, res) => {++ const robot = await domain.robots.get(req.params.id);++ if (!robot) fail(404, 'bot.robot_not_found', 'no such robot');++ if (req.principal.kind === 'service') {++ if (!apiAuth.granted(req.principal, CAP.control)) fail(403, 'capability.denied', `${CAP.control} not granted`);++ const subject = userSubject(req.query.subject, 'subject');++ return res.json({ subject, role: await domain.members.roleOf(robot.id, subject) });++ }++ fence(req);++ const mine = me(req);++ if (req.query.subject && req.query.subject !== mine) fail(403, 'bot.forbidden', 'you may only ask for yourself');++ return res.json({ subject: mine, role: await domain.members.roleOf(robot.id, mine) });++ }));r.post('/robots/:id/operators', manage, wrap(async (req, res) => {await owner(req, req.params.id);const subject = userSubject(req.body?.subject, 'subject');nnew file mode 100644@@@ -0,0 +1,49 @@++'use strict';++/**++ * Elements stage 1: GET /api/v1/robots/:id/role — the service that may control a robot (OpenVibe.Live,++ * sending a Viewer Deck press as the viewer) asks which role that viewer holds on it. A person may ask++ * about themself only, and a robot that does not exist is the same 404 for everyone.++ */++const assert = require('assert');++const { boot, check, done } = require('./helpers/app');++++(async () => {++ const t = await boot();++ const alex = t.network.newUser('alex');++ const operator = t.network.newUser('operator');++ const stranger = t.network.newUser('stranger');++++ await check('a service with bot.robot.control gets a named subject\'s role; read alone is refused', async () => {++ const { robot } = await t.robot(alex);++ const added = await t.call('POST', `/api/v1/robots/${robot.id}/operators`, { user: alex, body: { subject: operator.subject, role: 'operator' } });++ assert.strictEqual(added.status, 201, added.text);++ const svc = await t.call('GET', `/api/v1/robots/${robot.id}/role?subject=${operator.subject}`, { cap: ['bot.robot.control'] });++ assert.strictEqual(svc.status, 200, svc.text);++ assert.deepStrictEqual(svc.json, { subject: operator.subject, role: 'operator' });++ const owner = await t.call('GET', `/api/v1/robots/${robot.id}/role?subject=${alex.subject}`, { cap: ['bot.robot.control'] });++ assert.deepStrictEqual(owner.json, { subject: alex.subject, role: 'owner' });++ const none = await t.call('GET', `/api/v1/robots/${robot.id}/role?subject=${stranger.subject}`, { cap: ['bot.robot.control'] });++ assert.strictEqual(none.json.role, null, 'a non-member has no role, not an error');++ assert.strictEqual((await t.call('GET', `/api/v1/robots/${robot.id}/role?subject=${operator.subject}`, { cap: ['bot.robot.read'] })).status, 403);++ assert.strictEqual((await t.call('GET', `/api/v1/robots/${robot.id}/role?subject=${operator.subject}`, { cap: [] })).status, 403);++ });++++ await check('a service must name a subject, and a person may ask about themself only', async () => {++ const { robot } = await t.robot(alex);++ assert.strictEqual((await t.call('GET', `/api/v1/robots/${robot.id}/role`, { cap: ['bot.robot.control'] })).status, 422);++ const mine = await t.call('GET', `/api/v1/robots/${robot.id}/role?subject=${alex.subject}`, { user: alex });++ assert.strictEqual(mine.status, 200);++ assert.strictEqual(mine.json.role, 'owner');++ const other = await t.call('GET', `/api/v1/robots/${robot.id}/role?subject=${operator.subject}`, { user: alex });++ assert.strictEqual(other.status, 403);++ assert.strictEqual((await t.call('GET', `/api/v1/robots/${robot.id}/role`, { token: null })).status, 401);++ });++++ await check('an unknown robot is a 404, and no role answer leaks through it', async () => {++ assert.strictEqual((await t.call('GET', '/api/v1/robots/rob_00000000000000000000000000/role?subject=' + alex.subject, { cap: ['bot.robot.control'] })).status, 404);++ assert.strictEqual((await t.call('GET', '/api/v1/robots/rob_00000000000000000000000000/role?subject=' + alex.subject, { user: alex })).status, 404);++ });++++ await t.close();++ done();++})();
@@ -22,6 +22,7 @@
@@ -22,6 +22,7 @@
* | DELETE /robots/:id | bot.robot.manage | owner |
* | DELETE /robots/:id | bot.robot.manage | owner |
* | POST /robots/:id/pairing-code | bot.robot.manage | owner |
* | POST /robots/:id/pairing-code | bot.robot.manage | owner |
* | GET|POST /robots/:id/operators | bot.robot.read/manage | owner (write) / member (read) |
* | GET|POST /robots/:id/operators | bot.robot.read/manage | owner (write) / member (read) |
+ * | GET /robots/:id/role | bot.robot.control | a service asking about a subject, or a person asking about themself |
* | DELETE /robots/:id/operators/:subject | bot.robot.manage | owner |
* | DELETE /robots/:id/operators/:subject | bot.robot.manage | owner |
* | GET /robots/:id/devices | bot.robot.read | a member |
* | GET /robots/:id/devices | bot.robot.read | a member |
* | POST /devices/:id/rotate, /revoke | bot.device.connect | the robot's owner |
* | POST /devices/:id/rotate, /revoke | bot.device.connect | the robot's owner |
@@ -383,6 +384,22 @@ function v1Router({ domain, apiAuth, limits, hub, jobs: jobService, config, subm
@@ -383,6 +384,22 @@ function v1Router({ domain, apiAuth, limits, hub, jobs: jobService, config, subm
await member(req, req.params.id, ['owner', 'operator']);
await member(req, req.params.id, ['owner', 'operator']);
res.json({ operators: await domain.members.list(req.params.id) });
res.json({ operators: await domain.members.list(req.params.id) });
}));
}));
+ // Elements stage 1: the service that may control a robot (Live, sending a deck press as the viewer)
+ // asks which role that viewer holds on it. It is the same capability that lets it send the command, and
+ // the answer only says what Bot's own gate would already tell it. A person may ask about themself only.
+ r.get('/robots/:id/role', wrap(async (req, res) => {
+ const robot = await domain.robots.get(req.params.id);
+ if (!robot) fail(404, 'bot.robot_not_found', 'no such robot');
+ if (req.principal.kind === 'service') {
+ if (!apiAuth.granted(req.principal, CAP.control)) fail(403, 'capability.denied', `${CAP.control} not granted`);
+ const subject = userSubject(req.query.subject, 'subject');
+ return res.json({ subject, role: await domain.members.roleOf(robot.id, subject) });
+ }
+ fence(req);
+ const mine = me(req);
+ if (req.query.subject && req.query.subject !== mine) fail(403, 'bot.forbidden', 'you may only ask for yourself');
+ return res.json({ subject: mine, role: await domain.members.roleOf(robot.id, mine) });
+ }));
r.post('/robots/:id/operators', manage, wrap(async (req, res) => {
r.post('/robots/:id/operators', manage, wrap(async (req, res) => {
await owner(req, req.params.id);
await owner(req, req.params.id);
const subject = userSubject(req.body?.subject, 'subject');
const subject = userSubject(req.body?.subject, 'subject');
new file mode 100644
new file mode 100644
@@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
+'use strict';
+/**
+ * Elements stage 1: GET /api/v1/robots/:id/role — the service that may control a robot (OpenVibe.Live,
+ * sending a Viewer Deck press as the viewer) asks which role that viewer holds on it. A person may ask
+ * about themself only, and a robot that does not exist is the same 404 for everyone.
+ */
+const assert = require('assert');
+const { boot, check, done } = require('./helpers/app');
+
+(async () => {
+ const t = await boot();
+ const alex = t.network.newUser('alex');
+ const operator = t.network.newUser('operator');
+ const stranger = t.network.newUser('stranger');
+
+ await check('a service with bot.robot.control gets a named subject\'s role; read alone is refused', async () => {
+ const { robot } = await t.robot(alex);
+ const added = await t.call('POST', `/api/v1/robots/${robot.id}/operators`, { user: alex, body: { subject: operator.subject, role: 'operator' } });
+ assert.strictEqual(added.status, 201, added.text);
+ const svc = await t.call('GET', `/api/v1/robots/${robot.id}/role?subject=${operator.subject}`, { cap: ['bot.robot.control'] });
+ assert.strictEqual(svc.status, 200, svc.text);
+ assert.deepStrictEqual(svc.json, { subject: operator.subject, role: 'operator' });
+ const owner = await t.call('GET', `/api/v1/robots/${robot.id}/role?subject=${alex.subject}`, { cap: ['bot.robot.control'] });
+ assert.deepStrictEqual(owner.json, { subject: alex.subject, role: 'owner' });
+ const none = await t.call('GET', `/api/v1/robots/${robot.id}/role?subject=${stranger.subject}`, { cap: ['bot.robot.control'] });
+ assert.strictEqual(none.json.role, null, 'a non-member has no role, not an error');
+ assert.strictEqual((await t.call('GET', `/api/v1/robots/${robot.id}/role?subject=${operator.subject}`, { cap: ['bot.robot.read'] })).status, 403);
+ assert.strictEqual((await t.call('GET', `/api/v1/robots/${robot.id}/role?subject=${operator.subject}`, { cap: [] })).status, 403);
+ });
+
+ await check('a service must name a subject, and a person may ask about themself only', async () => {
+ const { robot } = await t.robot(alex);
+ assert.strictEqual((await t.call('GET', `/api/v1/robots/${robot.id}/role`, { cap: ['bot.robot.control'] })).status, 422);
+ const mine = await t.call('GET', `/api/v1/robots/${robot.id}/role?subject=${alex.subject}`, { user: alex });
+ assert.strictEqual(mine.status, 200);
+ assert.strictEqual(mine.json.role, 'owner');
+ const other = await t.call('GET', `/api/v1/robots/${robot.id}/role?subject=${operator.subject}`, { user: alex });
+ assert.strictEqual(other.status, 403);
+ assert.strictEqual((await t.call('GET', `/api/v1/robots/${robot.id}/role`, { token: null })).status, 401);
+ });
+
+ await check('an unknown robot is a 404, and no role answer leaks through it', async () => {
+ assert.strictEqual((await t.call('GET', '/api/v1/robots/rob_00000000000000000000000000/role?subject=' + alex.subject, { cap: ['bot.robot.control'] })).status, 404);
+ assert.strictEqual((await t.call('GET', '/api/v1/robots/rob_00000000000000000000000000/role?subject=' + alex.subject, { user: alex })).status, 404);
+ });
+
+ await t.close();
+ done();
+})();