openvibers/openvibe.bot Commits

Elements: a control service asks a person's role on a robot (#404)

OP OpenVibers committed 2 days ago · 205cfe87c0 · 2 files, +66 −0

Changes

ddiff --git a/server/api/v1.js b/server/api/v1.js
iindex e0866af..71c7b46 100644
---- a/server/api/v1.js
++++ b/server/api/v1.js
@@@ -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, subm
await 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');
ddiff --git a/test/element-role.test.js b/test/element-role.test.js
nnew file mode 100644
iindex 0000000..e41c252
---- /dev/null
++++ b/test/element-role.test.js
@@@ -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();
++})();