How to specify a Rule based on a Relationship

I’ve got a tasks collectionwith Schema:

{
  "title": "Task",
  "properties": {
    "_id": {
      "bsonType": "objectId"
    },
    "label": {
      "bsonType": "string"
    },
    "taskSet_id": {
      "bsonType": "objectId"
    },
}

and a TaskSet collection with schema:

{
  "title": "TaskSet",
  "properties": {
    "_id": {
      "bsonType": "objectId"
    },
    "owner_id": {
      "bsonType": "objectId"
    },
    "editor_ids": {
      "bsonType": "array",
      "items": {
        "bsonType": "objectId"
      }
    },
    "viewer_ids": {
      "bsonType": "array",
      "items": {
        "bsonType": "objectId"
      }
    }
  }
}

I then have a relationship:

{
  "taskSet_id": {
    "ref": "#/relationship/mongodb-atlas/ungant/TaskSet",
    "foreignKey": "_id",
    "isList": false
  }
}

I want to create an access Rule on Task with an Apply When:

{"taskSet_id.owner_id": "%%user.id"}

From the docs it says “App Services automatically replaces source values with the referenced objects or a null value in resolved GraphQL types and SDK models”, but my Rule does not appear to be working, and I get no results from JavaScript code when running collection.find({}).

I am admittedly confused by the whole Relationships topic, and how it relates to $lookup. I thought with the Relationship defined I might get the related document properties in results from a find(), but I do not. Can the Relationship be used for Rules permissions, and where else does it apply?

Hi, a relationship cannot be used within a rule expression. I will file a Documentation ticket to make this clear in the docs. The reason is simply that we can only use the data present within the document to evaluate rules, and pulling in all linked documents could result in significant performance degradation. Our recommendation would be to modify your data model to potentially duplicate data in order to achieve your permission goals if needed.

Best,
Tyler

Thanks Tyler, that makes sense. I will have potentially thousands of Tasks in a TaskSet, and access control is by TaskSet. With hundreds of users with access to a particular TaskSet, that would mean every one of the thousands of Tasks in a TaskSet contains the same list of hundreds of users with access permission. Is that a reasonable solution? I’m thinking it wouldn’t be great for performance or storage.

I also thought of making the TaskSet a document containing all the Tasks, which would solve the problem, but then the TaskSet contains an unbounded array.

A couple of options:

  • A TaskSet is a document containing up to 1000 Tasks, and if you add more, there is a link to a new document that contains the next 1000, and so on… Is that a reasonable solution, or considered bad practice?
  • Or, is the answer really that I need to run my own server rather than rely on Realm for this use case, where I could authenticate a user on the task set then serve all the Tasks without authenticating on each task?

Very grateful for any advice on this!

Thanks,
Dan

Hi, one other approach is to use custom user data and have each user document keep track of the TaskSet’s.

Therefore, you could structure your data like this:

type task {
     _id ObjectId
    name string 
    taskSet ObjectId
}

type taskSet {
    _id ObjectId
    ... blah 
}

type user {
     _id ObjectId
     taskSets: []ObjectId
}

And then structure permissions for the task collection to be:

{taskSet: {$in: "user.custom_data.taskSets"}}

Although it is not a one-to-one comparison, I think that reading through the Restricted News Feed example here might be helpful for you: https://www.mongodb.com/docs/atlas/app-services/sync/app-builder/device-sync-permissions-guide/#restricted-news-feed

Let me know if this solves your use case!

Best,
Tyler

Hi Tyler,

Thanks very much for the suggested structure and rule - that’s a neat proposal. I hadn’t considered writing the Rule that way around but that should work so I will go with that.

Cheers,

Dan

1 Like

Hi @Tyler, We’ve been using the scheme you suggested but now that our app is much further developed I’ve realised that we have a serious issue with this point that I overlooked before, as mentioned in the Restricted News Feed example you linked to:

“When a user subscribes or unsubscribes from an author, we update the array in the custom user data, but the changes don’t take effect until the current session is closed and a new session is started.”

This is a bit of a major for us.

Is there any way, either within an Atlas Function (where most of our DB access is consolidated) or from a web client, where we can force the current session to refresh without asking the user to log out and in again? Or anyway to force a refresh of the custom user data without creating a new session?

It’s not really acceptable in our use case to force the user to log out and in in order to change their permissions.

Thanks

We have been working on a feature to do just that. I will forward this along to someone who can update you on the progress of that and the estimated timeline.

Hi @Tyler_Kaye,

I’m facing the same issue as @Daniel_Bernasconi. I have 18 collections and I’d like to apply rules based on their relationships, but as I understand from your previous message, Atlas doesn’t currently support such rules.

I’m using a custom JWT as the authentication provider and created an endpoint that allows the authentication server to query all object IDs by collection for a specific user and put them in the payload. Now I’m using filters like:

{
  "uid": {
    "$in": "%%user.data.listofCollectionIds"
  }
}

These filters work, but they’re resource-consuming because I’m sending data already stored in Atlas back to it.

Inspired by https://www.mongodb.com/docs/atlas/app-services/sync/app-builder/device-sync-permissions-guide/, I changed the strategy and started using triggers on create and login events. These triggers fetch all object IDs associated with the user and create or update a new “custom_user_data” document with arrays of ObjectIds. Then the filters look like:

{
  "uid": {
    "$in": "%%user.custom_data.listofCollectionIds"
  }
}

I also added a filter on UserCustomData so that all the arrays of IDs are excluded when creating the access token but included while filtering, like:


{
  "_id": 1,
  "name": 1,
  "externalId": 1,
  "user_id": 1
}

With this new strategy, users with less data have a smooth access experience. However, for users with more data, Atlas complains about the size of the access token, as if all fields in UserCustomData are considered during token creation despite the filter.

I am looking forward to your insights soon.

Sincerely,

Egide

Hi, it sounds like the path you have taken is generally working for you, but let me know what you were looking to get some insight on.

When using custom-user data you can have lists of collection ids, but as you point out, when those lists become thousands (or tens of thousands) of lines long, then the performance can degrade a bit. At that point, its worth re-considering the data model you are using as any system that will rely on pulling I thousands of list elements to filter through will suffer from similar issues. Some things to think about in that case are attaching fields to the documents to be able to reference within the permissions expression (some sort of tags or reverse lookup on the userIds that can see it). However, this relies on how your application works.