openvibers/openvibe.bot Commits
Keep Actor browser storage private to Run’s leased desks (#347)
Changes
@@@ -801,7 +801,14 @@ function createRealtime({ config, keys, userAuth, log = console, now = () => Dat};const screen = msg.screen === true && kind === SCREEN_KIND;if (!screen && !COMPUTER_CAP[kind]) return refuse('bot.invalid_input', `kind is ${COMPUTER_KINDS.join(', ')}`);-- if (!screen) {++ const profileAction = kind === 'browser' && ['profile_export', 'profile_import', 'profile_sign_out'].includes(value.action);++ if (profileAction) {++ const lease = /^desk-[a-z0-9][a-z0-9-]{0,23}$/.test(value.desk || '') ? await domain.deskLeases.get(deviceId, value.desk) : null;++ if (conn.principal.kind !== 'service' || conn.principal.sub !== 'svc:run' || !lease || lease.owner !== `user:${subject}` || !capabilities.grants(conn.principal.cap, 'bot.desk.session')) return refuse('bot.forbidden', 'Saved sign-ins belong to Run and its leased desk.');++ const keys = value.action === 'profile_import' ? ['action', 'desk', 'profile'] : value.action === 'profile_sign_out' ? ['action', 'desk', 'site'] : ['action', 'desk'];++ if (Object.keys(value).some(k => !keys.includes(k)) || JSON.stringify(value).length > 48000 || (value.action === 'profile_import' && (!value.profile || !Array.isArray(value.profile.cookies) || !Array.isArray(value.profile.origins))) || (value.action === 'profile_sign_out' && !/^[a-z0-9.-]{1,253}$/.test(value.site || ''))) return refuse('bot.invalid_input', 'Invalid saved sign-ins.');++ }++ if (!screen && !profileAction) {const check = validate('bot.command@1', { v: 1, seq: 0, ts: 0, type: 'command', id, kind, value });if (!check.valid) return refuse('bot.invalid_input', `invalid ${kind}: ${(check.errors && check.errors[0] && (check.errors[0].message || JSON.stringify(check.errors[0]))) || 'bad value'}`.slice(0, 300));}@@@ -95,6 +95,14 @@ const { boot, check, done } = require('./helpers/app');assert.deepStrictEqual([r.status, r.json.result, r.json.desk], [200, 'ack', 'desk-s01'], r.text);assert.strictEqual(seen.at(-1).value.desk, 'desk-s01', 'Run never names the desk: Bot does');assert.strictEqual((await t.call('POST', url, { ...asRun(bob.subject), body: { kind: 'browser', value: { action: 'read', desk: 'desk-other' } } })).status, 422, 'a value naming a desk');++ for (const value of [{ action: 'profile_export' }, { action: 'profile_import', profile: { cookies: [], origins: [] } }, { action: 'profile_sign_out', site: 'example.org' }]) {++ const saved = await t.call('POST', url, { ...asRun(bob.subject), body: { kind: 'browser', value } });++ assert.equal(saved.status, 200, saved.text); assert.equal(saved.json.result, 'ack', saved.text);++ }++ assert.equal((await t.call('POST', url, { ...asRun(carol.subject), body: { kind: 'browser', value: { action: 'profile_export' } } })).status, 404);++ const privateCommand = await t.hub.machineCommand(own.deviceId, { kind: 'user', subject: bob.subject }, bob.subject, { kind: 'browser', value: { action: 'profile_export', desk: 'private' } });++ assert.equal(privateCommand.result, 'refused');++assert.strictEqual((await t.call('POST', url, { ...asRun(bob.subject), body: { kind: 'exec', value: { argv: ['id'] } } })).status, 422, 'never the machine itself');assert.strictEqual((await t.call('POST', url, { ...asRun(carol.subject), body: { kind: 'screen', value: {} } })).status, 404, 'another person');assert.strictEqual((await t.call('GET', `/api/v1/machines/${host.deviceId}/desk-leases/desk-s01`, asRun(carol.subject))).status, 404);
@@ -801,7 +801,14 @@ function createRealtime({ config, keys, userAuth, log = console, now = () => Dat
@@ -801,7 +801,14 @@ function createRealtime({ config, keys, userAuth, log = console, now = () => Dat
};
};
const screen = msg.screen === true && kind === SCREEN_KIND;
const screen = msg.screen === true && kind === SCREEN_KIND;
if (!screen && !COMPUTER_CAP[kind]) return refuse('bot.invalid_input', `kind is ${COMPUTER_KINDS.join(', ')}`);
if (!screen && !COMPUTER_CAP[kind]) return refuse('bot.invalid_input', `kind is ${COMPUTER_KINDS.join(', ')}`);
- if (!screen) {
+ const profileAction = kind === 'browser' && ['profile_export', 'profile_import', 'profile_sign_out'].includes(value.action);
+ if (profileAction) {
+ const lease = /^desk-[a-z0-9][a-z0-9-]{0,23}$/.test(value.desk || '') ? await domain.deskLeases.get(deviceId, value.desk) : null;
+ if (conn.principal.kind !== 'service' || conn.principal.sub !== 'svc:run' || !lease || lease.owner !== `user:${subject}` || !capabilities.grants(conn.principal.cap, 'bot.desk.session')) return refuse('bot.forbidden', 'Saved sign-ins belong to Run and its leased desk.');
+ const keys = value.action === 'profile_import' ? ['action', 'desk', 'profile'] : value.action === 'profile_sign_out' ? ['action', 'desk', 'site'] : ['action', 'desk'];
+ if (Object.keys(value).some(k => !keys.includes(k)) || JSON.stringify(value).length > 48000 || (value.action === 'profile_import' && (!value.profile || !Array.isArray(value.profile.cookies) || !Array.isArray(value.profile.origins))) || (value.action === 'profile_sign_out' && !/^[a-z0-9.-]{1,253}$/.test(value.site || ''))) return refuse('bot.invalid_input', 'Invalid saved sign-ins.');
+ }
+ if (!screen && !profileAction) {
const check = validate('bot.command@1', { v: 1, seq: 0, ts: 0, type: 'command', id, kind, value });
const check = validate('bot.command@1', { v: 1, seq: 0, ts: 0, type: 'command', id, kind, value });
if (!check.valid) return refuse('bot.invalid_input', `invalid ${kind}: ${(check.errors && check.errors[0] && (check.errors[0].message || JSON.stringify(check.errors[0]))) || 'bad value'}`.slice(0, 300));
if (!check.valid) return refuse('bot.invalid_input', `invalid ${kind}: ${(check.errors && check.errors[0] && (check.errors[0].message || JSON.stringify(check.errors[0]))) || 'bad value'}`.slice(0, 300));
}
}
@@ -95,6 +95,14 @@ const { boot, check, done } = require('./helpers/app');
@@ -95,6 +95,14 @@ const { boot, check, done } = require('./helpers/app');
assert.deepStrictEqual([r.status, r.json.result, r.json.desk], [200, 'ack', 'desk-s01'], r.text);
assert.deepStrictEqual([r.status, r.json.result, r.json.desk], [200, 'ack', 'desk-s01'], r.text);
assert.strictEqual(seen.at(-1).value.desk, 'desk-s01', 'Run never names the desk: Bot does');
assert.strictEqual(seen.at(-1).value.desk, 'desk-s01', 'Run never names the desk: Bot does');
assert.strictEqual((await t.call('POST', url, { ...asRun(bob.subject), body: { kind: 'browser', value: { action: 'read', desk: 'desk-other' } } })).status, 422, 'a value naming a desk');
assert.strictEqual((await t.call('POST', url, { ...asRun(bob.subject), body: { kind: 'browser', value: { action: 'read', desk: 'desk-other' } } })).status, 422, 'a value naming a desk');
+ for (const value of [{ action: 'profile_export' }, { action: 'profile_import', profile: { cookies: [], origins: [] } }, { action: 'profile_sign_out', site: 'example.org' }]) {
+ const saved = await t.call('POST', url, { ...asRun(bob.subject), body: { kind: 'browser', value } });
+ assert.equal(saved.status, 200, saved.text); assert.equal(saved.json.result, 'ack', saved.text);
+ }
+ assert.equal((await t.call('POST', url, { ...asRun(carol.subject), body: { kind: 'browser', value: { action: 'profile_export' } } })).status, 404);
+ const privateCommand = await t.hub.machineCommand(own.deviceId, { kind: 'user', subject: bob.subject }, bob.subject, { kind: 'browser', value: { action: 'profile_export', desk: 'private' } });
+ assert.equal(privateCommand.result, 'refused');
+
assert.strictEqual((await t.call('POST', url, { ...asRun(bob.subject), body: { kind: 'exec', value: { argv: ['id'] } } })).status, 422, 'never the machine itself');
assert.strictEqual((await t.call('POST', url, { ...asRun(bob.subject), body: { kind: 'exec', value: { argv: ['id'] } } })).status, 422, 'never the machine itself');
assert.strictEqual((await t.call('POST', url, { ...asRun(carol.subject), body: { kind: 'screen', value: {} } })).status, 404, 'another person');
assert.strictEqual((await t.call('POST', url, { ...asRun(carol.subject), body: { kind: 'screen', value: {} } })).status, 404, 'another person');
assert.strictEqual((await t.call('GET', `/api/v1/machines/${host.deviceId}/desk-leases/desk-s01`, asRun(carol.subject))).status, 404);
assert.strictEqual((await t.call('GET', `/api/v1/machines/${host.deviceId}/desk-leases/desk-s01`, asRun(carol.subject))).status, 404);