-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: validate role records #967
base: main
Are you sure you want to change the base?
Conversation
This reverts commit 56f7e18 and replaces it with an internal-only function, `getByDocIdIfExists`.
6722ede
to
335f048
Compare
I made a few changes based on discussions with @gmaclennan. I also did some quick profiling. I wrote a test that creates 20 managers and then invites them one by one. Manager 1 invites Manager 2, Manager 2 invites Manager 3, and so on. Expand this to see the test code.// This code is not perfect, but is good enough for this test.
test('perf test', async (t) => {
const managers = await createManagers(20, t)
const [creator] = managers
const projectId = await creator.createProject({ name: 'Mapeo' })
const disconnect = connectPeers(managers)
t.after(disconnect)
console.time('invite chain')
/** @type {undefined | MapeoManager} */
let previousManager
for (const manager of managers) {
if (previousManager) {
console.log(previousManager.deviceId, 'inviting', manager.deviceId)
await invite({
projectId,
invitor: previousManager,
invitees: [manager],
roleId: COORDINATOR_ROLE_ID,
})
}
previousManager = manager
}
console.timeEnd('invite chain')
const firstManager = managers[0]
const firstProject = await firstManager.getProject(projectId)
const lastManager = managers[19]
console.time('looking up role')
const lastManagerRole = await firstProject.$member.getById(
lastManager.deviceId
)
assert.equal(lastManagerRole.role.roleId, COORDINATOR_ROLE_ID)
console.timeEnd('looking up role')
}) Before this change, on my machine:
After this change:
Looking up the role is significantly slower, but I still think it's in an acceptable range. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work on this, the logic is tricky but clear enough to follow in review.
Can you add additional test(s) for a more complex scenario, e.g. a role chain of length > 2. I think it's ok to add a TODO for testing forked membership records. For forked membership records we also need to change the getWinner for membership records (to match the logic we use for looking up links
- a doc only has multiple links
if it was forked and then was subsequently merged.).
I think there are a few other cases that you are handling in the code that are not in the tests, it might be helpful to run a coverage test and see what code paths within the new code are being tested right now.
I am a bit concerned about the slow-down - it's a 40x slow down, and it could be more on phones depending on whether it is CPU limited or disk speed limited. If we start checking roles for reads of all data types, this could introduce a significant overhead. I think it's ok that we do not address this right now, and we think of a solution down the line when performance becomes an issue. Before we do that, we should spend some time adding perf metrics that get reported to Sentry.
src/roles.js
Outdated
const assignerCoreId = assignerCore.key.toString('hex') | ||
const assignerDeviceId = await this.#coreOwnership | ||
.getOwner(assignerCoreId) | ||
.catch(() => null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this maybe be nullIfNotFound
, and we should just allow it to throw if there is a different error?
currentMembershipRecord | ||
) | ||
switch (parentMembershipRecord) { | ||
case null: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe be more explicit and return false
here, since that is what will happen anyway.
switch (parentMembershipRecord) { | ||
case null: | ||
break | ||
case CREATOR_MEMBERSHIP_RECORD: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's slightly risky relying on strict equality here. We may make a change in the future that means that this is not strictly equal, and not realise the consequences.
I think we should match based on parentMembershipRecord.roleId === CREATOR_ROLE_ID
.
This means you can't write a switch statement, which is my hidden agenda for this comment... ha.
I do think having 3 if
statements will make this a bit more readable, with comments for each statement just explaining what that condition means.
switch (membershipRecord) { | ||
case null: | ||
return NO_ROLE | ||
case CREATOR_MEMBERSHIP_RECORD: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment below about relying on strict equality here.
src/roles.js
Outdated
/** @type {null | ReadonlyDeep<MembershipRecord>} */ | ||
let currentMembershipRecord = membershipRecord | ||
|
||
while (currentMembershipRecord) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should have a max-depth check, to avoid a bug/hack that might create a circular reference so that this never ends.
src/roles.js
Outdated
|
||
/** @type {null | typeof CREATOR_MEMBERSHIP_RECORD | MembershipRecord} */ | ||
let membershipRecordToCheck = latestMembershipRecord | ||
while (membershipRecordToCheck) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should have a max-depth check, to avoid a bug/hack that might create a circular reference so that this never ends.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I spent a little more time thinking about this and I think I need to change the approach.
Currently, if we encounter an invalid membership record, we treat that member as blocked. I think that's not bad because a member could write a bogus membership forever block a user from a project.
For example, imagine the following:
- Creator invites A and B, both as regular members.
- A claims that B is a coordinator. This is bogus because it lacks the permission.
Currently, this PR will treat B as blocked. Instead, I think we should ignore A's claim; B's role should still be "member".
In other words, I think we should ignore invalid membership records.
I'll try to implement that next week.
Depends on #968.
When fetching a role, we now validate that the role is valid, returning a blocked role if it's not.
See #188 for more details.