Skip to content
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: add project.leave() #410

Merged
merged 22 commits into from
Jan 11, 2024
Merged

feat: add project.leave() #410

merged 22 commits into from
Jan 11, 2024

Conversation

achou11
Copy link
Member

@achou11 achou11 commented Dec 7, 2023

Closes #241

TODO:

  • Delete indexed data from the project-specific database (will be follow-up)
  • Update sync mode? (may need a separate PR to introduce said sync mode) (will be follow-up)

@achou11
Copy link
Member Author

achou11 commented Dec 11, 2023

@gmaclennan do you mind taking a look at what's currently implemented here and let me know what glaring omissions there are? mostly have uncertainties around the steps that need to happen within the $leave() method (e.g. data deletion)

Copy link
Member

@gmaclennan gmaclennan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach is looking correct to me. However we don't currently have a way to assign a role, so it's not exactly working as expected - see #413. Initial fix in #414 but that just causes project.$leave() to throw right now.

This also raises questions for me about the implementation of Capabilities. The idea is that DEFAULT_CAPABILITIES will eventually move into the database, and can be edited by users. I think there are two separate types of roles:

  1. "invitable roles" - e.g. roles that someone with the right capability can assign to a new device in a project. These should also be the only roles that can be assigned with $member.assignRole().
  2. "special roles" used to implement internal details of the capability system - "creator", "blocked device" and "left of own choice". I think those assignments should only be used internally, e.g. we should implement a $member.block() method, and we have this project.$leave() method.

I think we should implement a capabilities.getRoles() function or similar which currently returns the corodinator and member roles, but in the future will read from the database. We can then use this as an assertion in assignRole().

I think maybe we give the "leave" role special treatment - any role can assign the leave role to themselves, apart from a device with the blocked role - hence blocked should be treated as a "special role" with slightly different rules.

I think this PR is blocked for now by #413 which we should resolve first.

src/mapeo-project.js Outdated Show resolved Hide resolved
src/mapeo-project.js Outdated Show resolved Hide resolved
- need to update leave tests to check data access after leaving (read+write)
- need to update leave tests to confirm that syncing auth still works
  after leaving
- need to add test for Capabilities.assignRole() change
@achou11
Copy link
Member Author

achou11 commented Jan 8, 2024

@gmaclennan do you mind taking another look at the changes here and providing input on what else is needed? Not sure what from #410 (review) should be done separately, and not sure if the TODOs I have in the changes diff reflect what's needed

@achou11 achou11 changed the title WIP: feat: add project.leave() feat: add project.leave() Jan 8, 2024
@achou11 achou11 marked this pull request as ready for review January 8, 2024 20:34
Comment on lines 79 to 80
// TODO: Does this make sense?
roleAssignment: [LEFT_ROLE_ID],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The more I'm looking at this, the more it seems that this change isn't necessary (i.e. NO_ROLE being able to assign LEFT), but would like confirmation before undoing

Comment on lines +153 to +174
[LEFT_ROLE_ID]: {
name: 'Left',
docs: mapObject(currentSchemaVersions, (key) => {
return [
key,
{
readOwn: false,
writeOwn: false,
readOthers: false,
writeOthers: false,
},
]
}),
roleAssignment: [],
sync: {
auth: 'allowed',
config: 'blocked',
data: 'blocked',
blobIndex: 'blocked',
blob: 'blocked',
},
},
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this an accurate representation for this role?

Comment on lines 593 to 596
this.#sharedDb
.delete(projectSettingsTable)
.where(eq(projectSettingsTable.docId, this.#projectId))
.run()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this makes sense but not entirely sure. My assumption is that once you leave a project, you shouldn't be able to access this kind of information, but maybe that's too strict in this case?

)
)

// 3. Clear data from project database
Copy link
Member Author

@achou11 achou11 Jan 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't implemented yet because I have questions about how this should be done. Is it just a naive db.delete(...)? (where db = new Database(this.#sqlite))

Wondering if the deletion of data in sqlite should be handled by something like CoreManager.deleteData()? A little bit tangled with the different boundaries and what should be responsible for this 😅

src/mapeo-project.js Outdated Show resolved Hide resolved
Copy link
Member

@gmaclennan gmaclennan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me, I think the behaviour is all correct, although there are a couple of changes that could be made to make it a bit less fragile and also a possible structure change:

  • Comparing roles based on the capability.name property is fragile if we start storing these records in the database (instead of hard-coding them) then we need to remember to never allow the name to change. Listing role records I think is a more robust way of doing what you want (I think that would work, but haven't tried it myself).
  • Possible change to whether we keep LEFT_ROLE_ID in the roleAssignment array, or treat it as a special case. I think it's fine as you have it, but we should document somewhere to make sure it is always included in roleAssignment if we use this approach.
  • Looking at all the parameters that now need to be passed to a project instance, I wonder whether it makes sense to split project.$leave into two methods? A private project[kleave]() method and manager.leaveProject(projectId). I think it's fine as it is, I think just that adding all these other parameters that a project instance must know about feels not quite right to me, but not sure if that's just aesthetics or there is a concrete reason for that.

I think the only thing missing is deleting the indexed data in the sqliteDb, but maybe that can be in a follow-up PR so that we can get this completed and merged?

}

if (
deviceIdToCaps[deviceId].name ===
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing this comparison on .name seems fragile - we would need to ensure that we never change .name in the codebase, or things would break.

I think what we need here is to do dataTypes.role.getAll(), which will return an array of records where docId === deviceId and you can read the roleId.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah yes, makes sense

Copy link
Member Author

@achou11 achou11 Jan 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is getting trickier than expected. feels like I'm doing some logic that's done by Capabilities.getAll() in order to account for edge cases 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

never mind, I figured it out 😄 addressed via 5fab0af

@@ -254,6 +288,10 @@ export class Capabilities {
* @param {keyof typeof DEFAULT_CAPABILITIES} roleId
*/
async assignRole(deviceId, roleId) {
if (deviceId !== this.#ownDeviceId && roleId === LEFT_ROLE_ID) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's ok to do it this way, although it does mean that we need to make it a hard requirement that all capability records can assign this (although there is the open question of whether a blocked user can also leave - they wouldn't be able to leave for other reasons, because they would not be able to write to the project any more).

An alternative approach would be to not include LEFT_ROLE_ID in the roleAssignments property and instead add this check to line 318 (if (!ownCapabilities.roleAssignment.includes(roleId)) {).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed via ca03ef3

@gmaclennan gmaclennan dismissed their stale review January 11, 2024 15:02

Good to merge and address anything else in follow up

@achou11 achou11 merged commit 42c90e4 into main Jan 11, 2024
7 checks passed
@achou11 achou11 deleted the 241/leave-project branch January 11, 2024 15:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

How do we leave a project?
2 participants