Creating a rights hierarchy in a graph structure

Hi,

I try to figure out how to create a rights hierachy as a graph structure in the MongoDB.
The idea is to have datasets which holds the information which “objects” has which right to an other “object”.

For example I store these information in the “rights” collection:

[
    {:_id 1 :name "Hugo" :rights []}
    {:_id 2 :name "Hermann" :rights [
        {:to 1 :type "write" :level 0}
    ]}
    {:_id 3 :name "Mauer" :rights [
        {:to 1 :type "write" :level 0}
        {:to 2 :type "read" :level 1}
    ]}
    {:_id 4 :name "Jon" :rights [
        {:to 1 :type "read" :level 1}
        {:to 2 :type "read" :level 1}
    ]}
    {:_id 5 :name "Jane" :rights [
        {:to 1 :type "write" :level 0}
        {:to 3 :type "write" :level 0}
        {:to 6 :type "read" :level 1}
        {:to 4 :type "read" :level 1}
    ]}
    {:_id 6 :name "Michael" :rights [
        {:to 4 :type "read" :level 1}
        {:to 5 :type "read" :level 1}
    ]}
]

I figured out, that I can use in restrictSearchWithMatch any condition even nested like {"rights.type" "write"} or {"rights.level" {$gte 1}}.
But I get not the results I wanted. For example, if I query for {"name" : "Michael"} and use in the “restrictSearchWithMatch” {"rights.level" {$gte 1}} I get not “Mauer” or “Hermann”.

How can I write the query in the manner, that every edge with the “read” type or level 1 is traversed?

Hello @Marcus_Lindner welcome to the community!

In this case the aggregation stage $graphLookup is your friend. Please checkout theses links

Cheers,
Michael

@michael_hoeller thanks for the warm welcome.

I read through the documentation and tried some ways, but the result was not what I wanted.

I program in Clojure with the Monger Library. So the Data I posted above has been stored in the collection “rights” as following documents:

{ "_id" : NumberLong(1), "name" : "Hugo", "rights" : [] }, { "_id" : NumberLong(2), "name" : "Hermann", "rights" : [ { "to" : NumberLong(1), "type" : "write", "level" : NumberLong(0) } ] }, { "_id" : NumberLong(3), "name" : "Mauer", "rights" : [ { "to" : NumberLong(1), "type" : "write", "level" : NumberLong(0) }, { "to" : NumberLong(2), "type" : "read", "level" : NumberLong(1) } ] }, { "_id" : NumberLong(4), "name" : "Jon", "rights" : [ { "to" : NumberLong(1), "type" : "read", "level" : NumberLong(1) }, { "to" : NumberLong(2), "type" : "read", "level" : NumberLong(1) } ] }, { "_id" : NumberLong(5), "name" : "Jane", "rights" : [ { "to" : NumberLong(1), "type" : "write", "level" : NumberLong(0) }, { "to" : NumberLong(3), "type" : "write", "level" : NumberLong(0) }, { "to" : NumberLong(6), "type" : "read", "level" : NumberLong(1) }, { "to" : NumberLong(4), "type" : "read", "level" : NumberLong(1) } ] }, { "_id" : NumberLong(6), "name" : "Michael", "rights" : [ { "to" : NumberLong(4), "type" : "read", "level" : NumberLong(1) }, { "to" : NumberLong(5), "type" : "read", "level" : NumberLong(1) } ] }

Foe the search I use following function:

(defn find-projected-graph [collection-name query & {:keys [depth as from-field to-field projection restrict-search-with-match]
                                                     :or   {depth                      Integer/MAX_VALUE
                                                            from-field                 :parent
                                                            to-field                   :_id
                                                            restrict-search-with-match {}
                                                            projection                 nil
                                                            as                         "Ontology"}}]
  (let [aggregation-array [{"$match" query}
                           {"$graphLookup" {:from                    collection-name
                                            :startWith               (str "$" (if (keyword? from-field) (name from-field) from-field))
                                            :connectFromField        from-field
                                            :connectToField          to-field
                                            :as                      as
                                            :maxDepth                depth
                                            :depthField              "numConnections"
                                            :restrictSearchWithMatch restrict-search-with-match
                                            }}]]
    (aggregate collection-name
               (if (nil? projection)
                 aggregation-array
                 (conj aggregation-array {"$project" projection})))))

(Short notes: In the Map after :or are the standard values defined. For example if I don’t define :to-field the value “connectToField” is the field “_id”. And the key values with the : are the field names so :_id is “_id” and Strings like “rights.to” are translated normally.)

If I now use following calls

(find-projected-graph "rights" {:name "Michael"} :from-field "rights.to" :restrict-search-with-match {"rights.type" "read"})

or

(find-projected-graph "rights" {:name "Michael"} :from-field "rights.to" :restrict-search-with-match {"rights.level" {$gte 1}})

I get the following result:

({:_id 6,
  :name "Michael",
  :rights [{:to 4, :type "read", :level 1} {:to 5, :type "read", :level 1}],
  :Ontology [{:_id 6,
              :name "Michael",
              :rights [{:to 4, :type "read", :level 1} {:to 5, :type "read", :level 1}],
              :numConnections 1}
             {:_id 3,
              :name "Mauer",
              :rights [{:to 1, :type "write", :level 0} {:to 2, :type "read", :level 1}],
              :numConnections 1}
             {:_id 5,
              :name "Jane",
              :rights [{:to 1, :type "write", :level 0}
                       {:to 3, :type "write", :level 0}
                       {:to 6, :type "read", :level 1}
                       {:to 4, :type "read", :level 1}],
              :numConnections 0}
             {:_id 4,
              :name "Jon",
              :rights [{:to 1, :type "read", :level 1} {:to 2, :type "read", :level 1}],
              :numConnections 0}]})

But I missing “Hermann” because “Mauer” has a “read” right to “Hermann”.
I think it is because “restrictSearchWithMatch” is a filter which is performed after the traversal of the graph.

The question is, exists a way to specify that the $graphllokup-function only considers “rights” which have the “type” “read” to find the next connection?
For example if I have this document

{ "_id" : NumberLong(3), "name" : "Mauer", "rights" : [ { "to" : NumberLong(1), "type" : "write", "level" : NumberLong(0) }, { "to" : NumberLong(2), "type" : "read", "level" : NumberLong(1) } ]
I get the link to the document with _id 2 but the link to document with the _id 1 is ignored.

The reason I ask this, I try to simplify a right-service which handles read and write access to objects but the rights are distributed between person, groups, and subgroups.

Welcome to the community, @Marcus_Lindner!

This can be achieved with this aggregation pipeline:

db.test1.aggregate([
  {
    // match the users, that you want to
    // join permissions on other users
    // (this stage can be omitted or modified as desired)
    $match: {
      name: 'Mauer'
    }
  },
  {
    // unwind the 'rights' array,
    // so it would be possible to join only the users,
    // that a given user has enough rights
    $unwind: '$rights',
  },
  {
    // filter the users to join here;
    // feel free to change the matching params
    $match: {
      'rights.type': 'read',
      'rights.level': {
        $gte: 1,
      }
    }
  },
  {
    $group: {
      _id: '$_id',
      name: {
        $first: '$name',
      },
      rights: {
        $push: '$rights',
      }
    }
  },
  {
    $graphLookup: {
      from: 'test1',
      startWith: '$rights.to',
      connectFromField: 'rights.to',
      // set maxDepth to 0, so
      // dependencies of the dependencies
      // would not join
      maxDepth: 0,
      connectToField: '_id',
      as: 'hasRequiredRightsOn',
    }
  },
  {
    // (optionally)
    // clean-up
    $project: {
      rights: false,
      'hasRequiredRightsOn.rights': false,
    }
  }
]).pretty();

Tested on the below dataset. I have prettified and reformatted it slightly, so it would be more readable and work in the mongo-shell. The data is kept untouched, though.

db.test1.insertMany([
  {
    _id: 1,
    name: "Hugo",
    rights: []
  },
  {
    _id: 2,
    name: "Hermann",
    rights: [
      { to: 1, type: "write", level: 0 }
    ]
  },
  {
    _id: 3,
    name: "Mauer",
    rights: [
      { to: 1, type: "write", level: 0 },
      { to: 2, type: "read", level: 1 }
    ]
  },
  {
    _id: 4,
    name: "Jon",
    rights: [
      { to: 1, type: "read", level: 1 },
      { to: 2, type: "read", level: 1 }
    ]
  },
  {
    _id: 5,
    name: "Jane",
    rights: [
      { to: 1, type: "write", level: 0 },
      { to: 3, type: "write", level: 0 },
      { to: 6, type: "read", level: 1 },
      { to: 4, type: "read", level: 1 }
    ]
  },
  {
    _id: 6,
    name: "Michael",
    rights: [
      { to: 4, type: "read", level: 1 },
      { to: 5, type: "read", level: 1 }
    ]
  }
]);

The output of the above aggregation will look like this (notice, that only required users are joined):

{
  "_id" : 3,
  "name" : "Mauer",
  "hasRequiredRightsOn" : [
    {
      "_id" : 2,
      "name" : "Hermann"
    }
  ]
}

You can include additional fields into the output with the $project stage.

Hello @slava,

thanks for the welcome and thanks for the answer.
Your code works and does what I need.

Thanks for your help.

This topic was automatically closed 5 days after the last reply. New replies are no longer allowed.