Skip to content

Commit 9cfdd0c

Browse files
authored
fix: only the project creator can change their role (#960)
(This diff looks large, but it's just one line of source code and a bunch of tests.) We have some code like this: ``` if (isAssigningProjectCreatorRole && !this.#isProjectCreator()) { ``` The intent: only allow the project creator to change their own role. However, `this.#isProjectCreator` returned a `Promise`, which meant that the second part of the condition *always* evaluated to `false`, which meant that the whole condition always evaluated to false, which meant that non-creators could change the creator's role. This fixes that by making `#isProjectCreator` return a `boolean`, not `Promise<boolean>`. Found this while working on [#188]. [#188]: #188
1 parent 6a4d0f4 commit 9cfdd0c

File tree

2 files changed

+124
-14
lines changed

2 files changed

+124
-14
lines changed

src/roles.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ export class Roles extends TypedEmitter {
408408
}
409409
}
410410

411-
async #isProjectCreator() {
411+
#isProjectCreator() {
412412
const ownAuthCoreId = this.#coreManager
413413
.getWriterCore('auth')
414414
.key.toString('hex')

test-e2e/members.js

Lines changed: 123 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { once } from 'node:events'
66
import {
77
COORDINATOR_ROLE_ID,
88
CREATOR_ROLE,
9+
CREATOR_ROLE_ID,
910
ROLES,
1011
MEMBER_ROLE_ID,
1112
NO_ROLE,
@@ -18,6 +19,8 @@ import {
1819
waitForSync,
1920
} from './utils.js'
2021
import { kDataTypes } from '../src/mapeo-project.js'
22+
/** @import { MapeoProject } from '../src/mapeo-project.js' */
23+
/** @import { RoleId } from '../src/roles.js' */
2124

2225
test('getting yourself after creating project', async (t) => {
2326
const [manager] = await createManagers(1, t, 'tablet')
@@ -355,8 +358,8 @@ test('roles - getMany() on newly invited device before sync', async (t) => {
355358
})
356359

357360
test('roles - assignRole()', async (t) => {
358-
const managers = await createManagers(2, t)
359-
const [invitor, invitee] = managers
361+
const managers = await createManagers(3, t)
362+
const [invitor, invitee, invitee2] = managers
360363
const disconnectPeers = connectPeers(managers)
361364
t.after(disconnectPeers)
362365

@@ -365,21 +368,48 @@ test('roles - assignRole()', async (t) => {
365368
await invite({
366369
invitor,
367370
projectId,
368-
invitees: [invitee],
371+
invitees: [invitee, invitee2],
369372
roleId: MEMBER_ROLE_ID,
370373
})
371374

372375
const projects = await Promise.all(
373376
managers.map((m) => m.getProject(projectId))
374377
)
375378

376-
const [invitorProject, inviteeProject] = projects
379+
const [invitorProject, inviteeProject, invitee2Project] = projects
380+
381+
/**
382+
* @param {MapeoProject} project
383+
* @param {string} otherDeviceId
384+
* @param {RoleId} expectedRoleId
385+
* @param {string} message
386+
* @returns {Promise<void>}
387+
*/
388+
const assertRole = async (
389+
project,
390+
otherDeviceId,
391+
expectedRoleId,
392+
message
393+
) => {
394+
assert.equal(
395+
(await project.$member.getById(otherDeviceId)).role.roleId,
396+
expectedRoleId,
397+
message
398+
)
399+
}
377400

378-
assert.deepEqual(
379-
(await invitorProject.$member.getById(invitee.deviceId)).role,
380-
ROLES[MEMBER_ROLE_ID],
401+
await assertRole(
402+
invitorProject,
403+
invitee.deviceId,
404+
MEMBER_ROLE_ID,
381405
'invitee has member role from invitor perspective'
382406
)
407+
await assertRole(
408+
invitorProject,
409+
invitee2.deviceId,
410+
MEMBER_ROLE_ID,
411+
'invitee 2 has member role from invitor perspective'
412+
)
383413

384414
assert.deepEqual(
385415
await inviteeProject.$getOwnRole(),
@@ -410,9 +440,10 @@ test('roles - assignRole()', async (t) => {
410440

411441
await waitForSync(projects, 'initial')
412442

413-
assert.deepEqual(
414-
(await invitorProject.$member.getById(invitee.deviceId)).role,
415-
ROLES[COORDINATOR_ROLE_ID],
443+
await assertRole(
444+
invitorProject,
445+
invitee.deviceId,
446+
COORDINATOR_ROLE_ID,
416447
'invitee now has coordinator role from invitor perspective'
417448
)
418449

@@ -447,9 +478,10 @@ test('roles - assignRole()', async (t) => {
447478

448479
await waitForSync(projects, 'initial')
449480

450-
assert.deepEqual(
451-
(await invitorProject.$member.getById(invitee.deviceId)).role,
452-
ROLES[MEMBER_ROLE_ID],
481+
await assertRole(
482+
invitorProject,
483+
invitee.deviceId,
484+
MEMBER_ROLE_ID,
453485
'invitee now has member role from invitor perspective'
454486
)
455487

@@ -459,6 +491,84 @@ test('roles - assignRole()', async (t) => {
459491
'invitee now has member role from invitee perspective'
460492
)
461493
})
494+
495+
await t.test(
496+
'regular members cannot assign roles to coordinator',
497+
async () => {
498+
await Promise.all(
499+
[invitorProject, inviteeProject, invitee2Project].flatMap((project) => [
500+
assertRole(
501+
project,
502+
invitee.deviceId,
503+
MEMBER_ROLE_ID,
504+
'test setup: everyone believes invitee 1 is a regular member'
505+
),
506+
assertRole(
507+
project,
508+
invitee2.deviceId,
509+
MEMBER_ROLE_ID,
510+
'test setup: everyone believes invitee 2 is a regular member'
511+
),
512+
])
513+
)
514+
515+
await assert.rejects(() =>
516+
inviteeProject.$member.assignRole(invitee.deviceId, COORDINATOR_ROLE_ID)
517+
)
518+
await assert.rejects(() =>
519+
inviteeProject.$member.assignRole(
520+
invitee2.deviceId,
521+
COORDINATOR_ROLE_ID
522+
)
523+
)
524+
525+
await waitForSync(projects, 'initial')
526+
527+
await Promise.all(
528+
[invitorProject, inviteeProject, invitee2Project].flatMap((project) => [
529+
assertRole(
530+
project,
531+
invitee.deviceId,
532+
MEMBER_ROLE_ID,
533+
'everyone believes invitee 1 is a regular member, even after attempting to assign higher role'
534+
),
535+
assertRole(
536+
project,
537+
invitee2.deviceId,
538+
MEMBER_ROLE_ID,
539+
'everyone believes invitee 2 is a regular member, even after attempting to assign higher role'
540+
),
541+
])
542+
)
543+
}
544+
)
545+
546+
await t.test(
547+
'non-creator members cannot change roles of creator',
548+
async () => {
549+
await invitorProject.$member.assignRole(
550+
invitee.deviceId,
551+
COORDINATOR_ROLE_ID
552+
)
553+
await waitForSync(projects, 'initial')
554+
555+
await assert.rejects(() =>
556+
inviteeProject.$member.assignRole(invitor.deviceId, COORDINATOR_ROLE_ID)
557+
)
558+
559+
await waitForSync(projects, 'initial')
560+
await Promise.all(
561+
[invitorProject, inviteeProject, invitee2Project].map((project) =>
562+
assertRole(
563+
project,
564+
invitor.deviceId,
565+
CREATOR_ROLE_ID,
566+
'everyone still believes creator to be a creator'
567+
)
568+
)
569+
)
570+
}
571+
)
462572
})
463573

464574
test('roles - assignRole() with forked role', async (t) => {

0 commit comments

Comments
 (0)