History read privilege for role?

In the Fauna docs for role document it says that history_read privilege is “not currently implemented”. What’s the status?

I’d need to have history_read privilege predicate for a role. The case is that the users sometimes need to read documents (using at(time)) that have been deleted. It seems to work if I add history_read predicate that just returns true: the user can read the old version of the document. But if I try to access the document in the predicate function, it seems to be just null. Shouldn’t the predicate function see the history-version of the document?

Hi @olazabal. Can you please provide the exact Role definition (FQL or FSL, whatever you are using) that includes the predicate that is not working as you expect?

I am working on clarification of what is implemented and what might not be.

Thanks!

privileges Objects {
	read {
		predicate ((obj) => {
			let projectId = obj.project.id;
			getPermission(projectId).p > 0;
		})
	}

	history_read {
		predicate ((obj) => {
			let projectId = obj.project.id;
			getPermission(projectId).p > 0;
		})
	}		
}

If I have some document that has been deleted and then try to read it using time when it still existed:

at(time) {
	Objects.byId(someId)
}

It just returns permission denied. It seems to me that the obj passed to history_read predicate is null if it does not exist in the present, while I’m expecting to get the history version of obj.

Thanks for the additional details.

The history_read permission is implemented in v10, so we need to get the documentation updated.

Importantly, the document passed to the history_read predicate is the latest, active version, not the version at the snapshot time. This is intentional. If we changed the reads to use the at time it would open you up to issues, for example, if you wanted to remove access to some data, then someone might be able to read information in the past when the current state says they shouldn’t. By always using the latest version, we ensure that any permissions rely on the latest changes, and you don’t have any such security loop holes.

If you delete a document, there is no active version, and the predicate will have no document to work with.

workarounds

The simplest way around this may be to create a readDeletedObject Function with permissions to read all history, then check for permissions within the Function and abort if permission check fails.

Predicates can make additional database reads. You may be able to work around the limitation by reading the latest version before deletion, and using that for permissions. Similar to the Function, but more built into the Role.

These would require the v4 Events function, which is not implemented yet in v10. use before: null to get the last page of events.

Paginate(Events(Ref(Collection("things"), "1234")), { before: null })

{
  after: {
    ts: 9223372036854776000,
    action: "add"
  },
  data: [
    {
      ts: 1708550326776000,
      action: "create",
      document: Ref(Collection("things"), "1234"),
      data: {
        value: 3,
        updated: true
      }
    },
    {
      ts: 1710947028140000,
      action: "update",
      document: Ref(Collection("things"), "1234"),
      data: null
    },
    {
      ts: 1710947073500000,
      action: "update",
      document: Ref(Collection("things"), "1234"),
      data: {
        value: 5
      }
    },
    {
      ts: 1710949299303000,
      action: "delete",
      document: Ref(Collection("things"), "1234"),
      data: null
    }
  ]
}
Let(
  {
    id: "1234",
    ref: Ref(Collection("things"), Var("id")),
    recentEvents: Select("data", Paginate(Events(Var("ref")), { size: 2, before: null })),
    removeDeletes: Filter(Var("recentEvents"), e => Not(Equals("delete", Select("action", e)))),
    latestNonDeleteEvent: Select(Add(-1, Count(Var("removeDeletes"))), Var("removeDeletes")),
    ts: Select("ts", Var("latestNonDeleteEvent"))
  },
  At(Var("ts"), Get(Var("ref")))
)

{
  ref: Ref(Collection("things"), "1234"),
  ts: 1710947073500000,
  data: {
    value: 5,
    updated: true
  }
}

Or maybe you could have a kind of “ObjectsTrash” collection that you would send documents to when they are deleted, and you could read/restore from there.

If you create a Function purely to proxy the Events, then you can define all the other logic in v10 and it’s MUCH more readable.

image

v4 Function definition

Query(
  Lambda(
    ["ref", "params"],
    Let(
      {
        size: Select("size", Var("params"), 64),
        before: Select("before", Var("params"), "$MISSING"),
        after: Select("after", Var("params"), "$MISSING"),
        events: Events(Var("ref"))
      },
      If(
        Equals(Var("before"), "$MISSING"),
        If(
          Equals(Var("after"), "$MISSING"),
          Paginate(Var("events"), { size: Var("size") }),
          Paginate(Var("events"), { after: Var("after"), size: Var("size") })
        ),
        Paginate(Var("events"), { before: Var("before"), size: Var("size") })
      )
    )
  )
)

Thanks for the clarification and the workarounds. It all makes sense. I used the workaround to wrap the history read in a function, allow the user to call it, run it as admin, check permissions there and abort if needed.