diff --git a/src/roles.js b/src/roles.js index c77bfbac..a7fc8dfb 100644 --- a/src/roles.js +++ b/src/roles.js @@ -98,6 +98,33 @@ export const CREATOR_ROLE = { }, } +/** + * @type {Role} + */ +const BLOCKED_ROLE = { + roleId: BLOCKED_ROLE_ID, + name: 'Blocked', + docs: mapObject(currentSchemaVersions, (key) => { + return [ + key, + { + readOwn: false, + writeOwn: false, + readOthers: false, + writeOthers: false, + }, + ] + }), + roleAssignment: [], + sync: { + auth: 'blocked', + config: 'blocked', + data: 'blocked', + blobIndex: 'blocked', + blob: 'blocked', + }, +} + /** * This is the role assumed for a device when no role record can be found. This * can happen when an invited device did not manage to sync with the device that @@ -166,29 +193,7 @@ export const ROLES = { blob: 'allowed', }, }, - [BLOCKED_ROLE_ID]: { - roleId: BLOCKED_ROLE_ID, - name: 'Blocked', - docs: mapObject(currentSchemaVersions, (key) => { - return [ - key, - { - readOwn: false, - writeOwn: false, - readOthers: false, - writeOthers: false, - }, - ] - }), - roleAssignment: [], - sync: { - auth: 'blocked', - config: 'blocked', - data: 'blocked', - blobIndex: 'blocked', - blob: 'blocked', - }, - }, + [BLOCKED_ROLE_ID]: BLOCKED_ROLE, [LEFT_ROLE_ID]: { roleId: LEFT_ROLE_ID, name: 'Left', @@ -281,7 +286,7 @@ export class Roles extends TypedEmitter { const { roleId } = roleAssignment if (!isRoleId(roleId)) { - return ROLES[BLOCKED_ROLE_ID] + return BLOCKED_ROLE } return ROLES[roleId] } @@ -403,7 +408,7 @@ export class Roles extends TypedEmitter { } } - async #isProjectCreator() { + #isProjectCreator() { const ownAuthCoreId = this.#coreManager .getWriterCore('auth') .key.toString('hex') diff --git a/test-e2e/members.js b/test-e2e/members.js index b47f61f5..bdc0c9b8 100644 --- a/test-e2e/members.js +++ b/test-e2e/members.js @@ -6,6 +6,7 @@ import { once } from 'node:events' import { COORDINATOR_ROLE_ID, CREATOR_ROLE, + CREATOR_ROLE_ID, ROLES, MEMBER_ROLE_ID, NO_ROLE, @@ -18,6 +19,8 @@ import { waitForSync, } from './utils.js' import { kDataTypes } from '../src/mapeo-project.js' +/** @import { MapeoProject } from '../src/mapeo-project.js' */ +/** @import { RoleId } from '../src/roles.js' */ test('getting yourself after creating project', async (t) => { const [manager] = await createManagers(1, t, 'tablet') @@ -355,8 +358,8 @@ test('roles - getMany() on newly invited device before sync', async (t) => { }) test('roles - assignRole()', async (t) => { - const managers = await createManagers(2, t) - const [invitor, invitee] = managers + const managers = await createManagers(3, t) + const [invitor, invitee, invitee2] = managers const disconnectPeers = connectPeers(managers) t.after(disconnectPeers) @@ -365,7 +368,7 @@ test('roles - assignRole()', async (t) => { await invite({ invitor, projectId, - invitees: [invitee], + invitees: [invitee, invitee2], roleId: MEMBER_ROLE_ID, }) @@ -373,13 +376,40 @@ test('roles - assignRole()', async (t) => { managers.map((m) => m.getProject(projectId)) ) - const [invitorProject, inviteeProject] = projects + const [invitorProject, inviteeProject, invitee2Project] = projects + + /** + * @param {MapeoProject} project + * @param {string} otherDeviceId + * @param {RoleId} expectedRoleId + * @param {string} message + * @returns {Promise} + */ + const assertRole = async ( + project, + otherDeviceId, + expectedRoleId, + message + ) => { + assert.equal( + (await project.$member.getById(otherDeviceId)).role.roleId, + expectedRoleId, + message + ) + } - assert.deepEqual( - (await invitorProject.$member.getById(invitee.deviceId)).role, - ROLES[MEMBER_ROLE_ID], + await assertRole( + invitorProject, + invitee.deviceId, + MEMBER_ROLE_ID, 'invitee has member role from invitor perspective' ) + await assertRole( + invitorProject, + invitee2.deviceId, + MEMBER_ROLE_ID, + 'invitee 2 has member role from invitor perspective' + ) assert.deepEqual( await inviteeProject.$getOwnRole(), @@ -410,9 +440,10 @@ test('roles - assignRole()', async (t) => { await waitForSync(projects, 'initial') - assert.deepEqual( - (await invitorProject.$member.getById(invitee.deviceId)).role, - ROLES[COORDINATOR_ROLE_ID], + await assertRole( + invitorProject, + invitee.deviceId, + COORDINATOR_ROLE_ID, 'invitee now has coordinator role from invitor perspective' ) @@ -447,9 +478,10 @@ test('roles - assignRole()', async (t) => { await waitForSync(projects, 'initial') - assert.deepEqual( - (await invitorProject.$member.getById(invitee.deviceId)).role, - ROLES[MEMBER_ROLE_ID], + await assertRole( + invitorProject, + invitee.deviceId, + MEMBER_ROLE_ID, 'invitee now has member role from invitor perspective' ) @@ -459,6 +491,84 @@ test('roles - assignRole()', async (t) => { 'invitee now has member role from invitee perspective' ) }) + + await t.test( + 'regular members cannot assign roles to coordinator', + async () => { + await Promise.all( + [invitorProject, inviteeProject, invitee2Project].flatMap((project) => [ + assertRole( + project, + invitee.deviceId, + MEMBER_ROLE_ID, + 'test setup: everyone believes invitee 1 is a regular member' + ), + assertRole( + project, + invitee2.deviceId, + MEMBER_ROLE_ID, + 'test setup: everyone believes invitee 2 is a regular member' + ), + ]) + ) + + await assert.rejects(() => + inviteeProject.$member.assignRole(invitee.deviceId, COORDINATOR_ROLE_ID) + ) + await assert.rejects(() => + inviteeProject.$member.assignRole( + invitee2.deviceId, + COORDINATOR_ROLE_ID + ) + ) + + await waitForSync(projects, 'initial') + + await Promise.all( + [invitorProject, inviteeProject, invitee2Project].flatMap((project) => [ + assertRole( + project, + invitee.deviceId, + MEMBER_ROLE_ID, + 'everyone believes invitee 1 is a regular member, even after attempting to assign higher role' + ), + assertRole( + project, + invitee2.deviceId, + MEMBER_ROLE_ID, + 'everyone believes invitee 2 is a regular member, even after attempting to assign higher role' + ), + ]) + ) + } + ) + + await t.test( + 'non-creator members cannot change roles of creator', + async () => { + await invitorProject.$member.assignRole( + invitee.deviceId, + COORDINATOR_ROLE_ID + ) + await waitForSync(projects, 'initial') + + await assert.rejects(() => + inviteeProject.$member.assignRole(invitor.deviceId, COORDINATOR_ROLE_ID) + ) + + await waitForSync(projects, 'initial') + await Promise.all( + [invitorProject, inviteeProject, invitee2Project].map((project) => + assertRole( + project, + invitor.deviceId, + CREATOR_ROLE_ID, + 'everyone still believes creator to be a creator' + ) + ) + ) + } + ) }) test('roles - assignRole() with forked role', async (t) => { diff --git a/test-e2e/sync.js b/test-e2e/sync.js index 95fb1a43..72a0bf25 100644 --- a/test-e2e/sync.js +++ b/test-e2e/sync.js @@ -855,6 +855,12 @@ test('Correct sync state prior to data sync', async function (t) { managers.map((m) => m.getProject(projectId)) ) + for (const project of projects) { + const { remoteDeviceSyncState } = project.$sync.getState() + const otherDeviceCount = Object.keys(remoteDeviceSyncState).length + assert.equal(otherDeviceCount, COUNT - 1) + } + const generated = await seedDatabases(projects, { schemas: ['observation'] }) await waitForSync(projects, 'initial') diff --git a/test/data-type.js b/test/data-type.js index 8987c990..1559f702 100644 --- a/test/data-type.js +++ b/test/data-type.js @@ -114,6 +114,15 @@ test('private createWithDocId() method throws when doc exists', async () => { ) }) +test('getByVersionId fetches docs by their version ID', async () => { + const { dataType } = await testenv() + + const created = await dataType.create(obsFixture) + const fetched = await dataType.getByVersionId(created.versionId) + + assert.equal(created.docId, fetched.docId) +}) + test('`originalVersionId` field', async () => { const { dataType, dataStore } = await testenv()