Flexible Sync field level permissions based on another field

I would like a recommendation on the best way to track field level permissions with Flexible Sync.

I’m using Swift Realm SDK. My objects are as follows…

class House: Object {
    @Persisted(primaryKey: true) var _id: ObjectId
    @Persisted var owner_id: String
    @Persisted var avatarURL: String
    @Persisted var caretakers: List<User>
}

final class User: Object {
    @Persisted(primaryKey: true) var _id: ObjectId
    
    @Persisted(originProperty: "caretakers") var caretakerFor: LinkingObjects<House>
}

Here, I want the owner (owner_id) to have read and write access to avatarURL, but anyone in the caretakers list to only have read access to the field.

When setting my Flexible Sync role permissions, I tried something along the following…

"rules": {
    "House": [
      {
        "name": "all", // This already feels incorrect, but I can't set the `applyWhen` based on document data since it is set at the start of the session.
        "applyWhen": {},
        "read": true,
        "write": true,
        "fields": {
          "avatarURL": {
            "read": {
              "caretakers": {
                "%stringToOid": "%%user.id"
              } // Assume there is an "or" here for the owner
          },
            "write": { "owner_id": "%%user.id"  }
          }
        },
        "additional_fields": {
          "read": false,
          "write": false
        }
      }
    ]
  }

However, I receive an error: role field "fields.avatarURL.read" expects a value of type "boolean", but provided value was of type "object"

So it would appear I cannot dynamically set the permissions of a field based on another field.

This approach seems good in the context of my objects since when I open a House, I can see all the caretakers, and each User can easily see all the houses they are caretakers for (thanks to the object linking). However, this doesn’t seem to fit in with how we configure Flexible Sync permissions.

Is there something I’m missing that would make this approach work? Or is there another strategy for tracking permissions is recommended?

Hey Kevin,

Field-level permissions in flexible sync rules are currently limited to only booleans; hence, the error message you are seeing about how “fields.avatarURL.read” expects a boolean. The team is still evaluating implementing support for dynamic expressions in field-level permissions, and we appreciate the feedback given here.

I noticed in the role “all” that you have specified, only the “avatarURL” field can ever get read from or written to, due to the “additional_fields”:

"additional_fields": {
   "read": false,
   "write": false
}

I think as a workaround, you could define the role to instead be something like:

{
   "name": "all", 
   "applyWhen": {},
   "read": { 
       $or: [
          { "caretakers": {
              "%stringToOid":"%%user.id"
          }},
         {
           "owner_id": "%%user.id" 
         }
      ]
    },
    "write": { "owner_id":"%%user.id" },
    "fields": {
       "avatarURL": {
          "read": true,
          "write": true
       }
    },
    "additional_fields": {
      "read": false,
      "write": false
   } 
}

Or better yet, since write access implies read:

{
   "name": "all", 
   "applyWhen": {},
   "read": { "caretakers": { "%stringToOid":"%%user.id" } },
   "write": { "owner_id":"%%user.id" },
   "fields": {
      "avatarURL": {
         "read": true,
         "write": true
      }
   },
   "additional_fields": {
      "read": false,
      "write": false
   } 
}

Let me know if that works for you,
Jonathan

Hey Jonathan, thanks for the response.

Looks like that does work for my example. I tried to trim everything down for the sake of the post, and in reality, we have some fields that we want writable by caretakers. Let me expand on the example to give a more accurate picture.

So instead of just caretakers, we also have “family members”, that can add/remove caretakers. Also, let’s say we have a list for recording every time the lawn is mowed.

class House: Object {
    @Persisted(primaryKey: true) var _id: ObjectId
    @Persisted var owner_id: String
    @Persisted var avatarURL: String
    @Persisted var familyMembers: List<User>
    @Persisted var caretakers: List<User>
    @Persisted var lawnMowed: List<Date>
}

final class User: Object {
    @Persisted(primaryKey: true) var _id: ObjectId
    
    @Persisted(originProperty: "familyMember") var familyMemberFor: LinkingObjects<House>
    @Persisted(originProperty: "caretakers") var caretakerFor: LinkingObjects<House>
}

So in this case, familyMembers can read/write caretakers. Caretakers should be able to read/write to lawnMowed, but they should not be able to write avatarURL, or read/write caregivers itself.

Since our caretakers can write one of the fields, they would need to be included in the high level “write” block, which with the rules you provided, that would allow write access to the avatarURL field.

Ideally, I would like to make separate caretaker/familyMember/owner roles, but we can’t evaluate the document in the applyWhen. The other option would be going with the all role I showed above, but then we would need to dynamically evaluate the document for read/write at the field level.

What would you recommend for handling the permissions in this case? Would I be better off having a separate Permissions collection, and use a function within the applyWhen to assign a role?

Also, Not sure the solution you provided will work. caretakers is not a queryable field so I am not able to use it in the read/write.

Got it, yeah. caretakers and owner_id would indeed have to be queryable fields to make the solution I suggested work.

If possible, I would suggest moving out the fields caretakers, owner_id, and familyMembers into custom user data and defining roles like:

{
    "name": "caretaker",
    "applyWhen": { "%%user.custom_data.isCaretaker": true },
    ...
}

If that’s not possible, my other thought would be storing some notion of different permission accesses in the custom user data, (ie which fields each user is allowed to access):

{
    "canReadAvatarURL": true,
    "canReadCaretakers": true
}

and then you could define a role for every permissions scenario like:

{
    "name": "canReadAvatarURLAndCaretakersRole",
    "applyWhen": { $and: [ 
       { "%%user.custom_data.canReadAvatarURL": true },
       { "%%user.custom_data.canReadCaretakers": true }
    ]},
    "read": true,
    "write": true,
    "fields": {
       "avatarURL": {
           "read": true,
           "write": false
       },
       "caretakers": {
           "read": true,
           "write": false
       }
    },
    "additional_fields": {
        "read": false,
        "write": false
    },
}

Jonathan

Thanks for looking into it. However, I’m still not sure this addresses my issue. Correct me if I’m wrong, but what you are suggesting means that if a user canReadAvatarURL, then it is applied to all House documents. I need something that is a tiered privilege but on a per-document base, or at least a different way to structure our documents to allow for this.

I see, unfortunately I think the current system is pretty limited in supporting different field-level permission schemes based on the values of fields in the document. While I wouldn’t normally suggest this, as it is not really scalable or particularly elegant, I believe the only way of really accomplishing what you are trying to do then is to define a separate collection for each unique permission that you want to be able to capture, and leveraging the top-level read and write expressions in the role to dynamically control data access based on the value of document fields. Taking the example you gave of:

Here, I want the owner (owner_id) to have read and write access to avatarURL, 
but anyone in the caretakers list to only have read access to the field.

You could move out the avatarURL field and copy out the owner_id/caretakers/house_id fields into a separate collection HouseAvatar with a schema like:

{
    house_id: <_id-of-house>,
    owner_id: <owner_id>,
    caretakers: <caretakers>,
    avatarURL: <avatarURL>,
}

And a role for that collection might look like:

{
   "name": "role", 
   "applyWhen": {},
   "read": { "caretakers": { "%stringToOid":"%%user.id" } },
   "write": { "owner_id":"%%user.id" },
   "fields": {
      "avatarURL": {
         "read": true,
         "write": true
      }
   },
   "additional_fields": {
      "read": false,
      "write": false
   } 
}

Then, when you need to read/write the avatarURL associated with a particular house, you could lookup the house_id in HouseAvatar.

Even then, you would likely also need to define additional roles to control who can write caretakers and owner_id in HouseAvatar or introduce more collections altogether - since it sounds like those restrictions may dynamically depend on document fields as well.

As a follow up to my previous post, if it becomes challenging to enforce your data access control requirements via the existing sync permissions system, it may be easier to implement that control within the application logic, and would suggest exploring that as a possibility as well.

Best,
Jonathan