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

Add role records #189

Closed
3 of 5 tasks
Tracked by #204
gmaclennan opened this issue Aug 18, 2023 · 2 comments · Fixed by #231
Closed
3 of 5 tasks
Tracked by #204

Add role records #189

gmaclennan opened this issue Aug 18, 2023 · 2 comments · Fixed by #231
Assignees

Comments

@gmaclennan
Copy link
Member

gmaclennan commented Aug 18, 2023

Description

We need to assign a role to each device, which is used to determine what that device can do, e.g. can invite others, can remove others from project, can edit other peoples' data etc.

In the future we might want to support users being able to configure a project to decide what each role can do.

We need to be able to validate role assignments to trace them back to the project creator, to ensure that the device that made the role assignments had a role that gave them permission to do that.

I think we can keep this simple and keep role records to:

type Role = {
  docId: string, // should be auth core ID (hex of auth core key)
  roleId: string, // random 64-bit ID as hex string
  fromIndex: number
}

docId should be the core key of the auth core of the device that the role assignment applies to. I think it makes sense to use this rather than the device ID because it means that tracing a role does not require referencing the coreOwnership records.

roleId can be an arbitrary random ID. These roleIds should be defined in a Capability record (see below).

fromIndex should be the index of the auth core that this role applies to (e.g. the auth core with the key matching role.docId). This should normally be authCore.length, but there are use-cases for "back-dating" the fromIndex to ignore role assignments by a "bad actor", see #188 (comment)

For the MVP we will ignore the fromIndex. However this means that if a device is removed from a project then any device they have invited will also be removed. We should resolve this as soon after the MVP as possible - I don't think we should expose project removal in the UX of the MVP, since this is not a feature of current Mapeo anyway.

A device without an assigned role ID should be considered not in the project and blocked from sync.

Capability records

These define what each role can do in a project, and the name of the role. The docId for a capability record should be the roleId.

type Capability = {
  docId: string, // should be roleId
  // Human-readable name for role
  name: string,
  [${Exclude<MapeoDoc['schemaName'] | 'role' >}]?: Array<'readOwn' | 'writeOwn' | 'readOthers' | 'writeOthers'>
  roleAssignmentCapability: Array<string> // Array of role ids that someone with that role can assign
  sync: 'allowed' | 'blocked'
}

E.g. the default of two roles, coordinator and member, where only coordinators can invite others, and members can only edit their own data.

const coordinatorCapability = {
  docId: // role ID for coordinator role (can be random, maybe pre-determine this in app code as part of a template)
  name: 'coordinator',
  observation: ['readOwn', 'writeOwn', 'readOthers', 'writeOthers'],
  // needs capabilities for other data types defined
  roleAssignmentCapability: [coordinatorRoleId, memberRoleId],
  sync: 'allowed'
}

const memberCapability = {
  docId: // role ID for member role (can be random, maybe pre-determine this in app code as part of a template)
  name: 'member',
  observation: ['readOwn', 'writeOwn', 'readOthers'],
  roleAssignmentCapability: [],
  sync: 'allowed'
}

const blockedCapability = {
  docId: // role ID for blocked role
  name: 'blocked',
  sync: 'blocked'
}

I think for the MVP the Capability records don't need to be in the database - we can hardcode them as defaults.

Validation

Validating role records will require tracing the chain of role assignments back to the project creator. This feature will be added post-MVP. For more details of the implementation plan see #188

Write capabilities (writeOwn and writeOthers)

We can block this at an app-level, but we can't actually stop a device directly writing to their own database. However when indexing documents we can choose to ignore any documents that match one of these criteria (e.g. for writeOwn it would be the first document, or an update of a document, or for writeOthers it would be an update of a document created by another device).

fromIndex for other cores (other than auth core)

In the future we might want to add additional fields to the Role document for ${namespace}FromIndex, e.g. a fromIndex for each writer core of a device in each namespace. This would be valuable for ignoring data from a certain previous point in time (e.g. implementing writeOthers=false from a point in time) when a device had valuable data up to a point, that we want to keep, and then ignore data written past that point.

This is not an MVP feature and can be added later.

Read capabilities (readOwn and readOthers)

This can only initially be implemented at a runtime level - since this is a p2p system anyone with a synced copy could directly access the database and read the data. However the vast majority of users will only be interacting with the data through the apps, so app-level restrictions are enough.

In the future we can implement this via encrypted documents (e.g. document-level encryption using something like PGP, rather than the block-level encryption - with a single encryption key for the entire core - that we have right now). readOwn=false can be implemented with sealed box encryption.

Implementation plan

Create a new Capability class that takes authDataStore and coreOwnership instances as constructor parameters and initially has a single method: getCapabilities(deviceId) that should return a capability record above. Internally this class should create an instance of DataType with role.

We should initially implement default capability records, with some pre-defined role IDs.

const cap = new Capability({ authDataStore, coreOwnership })
const capabilities = cap.getCapabilities(deviceId)
if (capabilities.sync === 'allowed') {
   // continue with sync with device
}

Initial idea for getCapabilities()

{
  async getCapabilities(deviceId) {
    const authCoreKey = await this.coreOwnership.getCoreKey(deviceId, 'auth')
    const { roleId } = await this.#roleDataType.getByDocId(authCoreKey.toString('hex'))
    const capabilities = DEFAULT_CAPABILITIES[roleId]
  }
}

Tasks

  • Add dataType.createWithDocId() method #190
  • Add core ownership records #175
  • digidem/mapeo-schema#124
  • Add default role IDs and capabilities
  • Handle duplicate role records, e.g. two different devices have invited a device to a project and both have written role records that do not link to each other. I think the default of the getWinner that just chooses the most recent would be ok?
@achou11
Copy link
Member

achou11 commented Aug 21, 2023

Overall this makes sense to me!

One potential suggestion for the Capability type: any reason against a top-level specific field that holds the datatype-specific permissions? for example:

type Capability = {
  docId: string, // should be roleId
  // Human-readable name for role
  name: string,
  // 👇specify a top-level field that holds information that can vary per application
  dataTypes: {
    [${Exclude<MapeoDoc['schemaName'] | 'role' >}]?: Array<'readOwn' | 'writeOwn' | 'readOthers' | 'writeOthers'>
  },
  roleAssignmentCapability: Array<string> // Array of role ids that someone with that role can assign
  sync: 'allowed' | 'blocked'
}

I don't really have any algorithmic reasoning for this but given that datatypes can technically vary across applications, it seems helpful to have a consistent field that will hold that dynamic information related to data types.

EDIT: Maybe permissions or some inclusion of that word is better for the field name?

gmaclennan added a commit to digidem/comapeo-schema that referenced this issue Aug 23, 2023
* feat: Add updated `Role` data type

Fixes #124
For details see digidem/comapeo-core#189

* Update & add tests; fix things

* clarify description of `fromIndex`

Still not particularly clear, but hopefully a bit more so
gmaclennan added a commit to digidem/comapeo-schema that referenced this issue Aug 23, 2023
* feat: Add updated `Role` data type

Fixes #124
For details see digidem/comapeo-core#189

* Update & add tests; fix things

* feat: device -> deviceInfo & new schema

Fixes #126

For more info see digidem/comapeo-core#182

* add tests
@gmaclennan gmaclennan self-assigned this Aug 24, 2023
@gmaclennan gmaclennan linked a pull request Aug 31, 2023 that will close this issue
@gmaclennan
Copy link
Member Author

Closed via #231

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 a pull request may close this issue.

2 participants