Privacy Rules
Last updated
Was this helpful?
Last updated
Was this helpful?
A crucial reason on why Ent Framework exists at all is its privacy layer. No data exits the API unless it's rechecked against a set of explicitly defined security predicates. In other words, when you have multiple users in your service, you can enforce the strict guarantees that one user can't see other user's data even in theory.
In relational databases world, the concept of per-row security rechecking is called "row-level security".
Before we continue, we must mention that some support for row-level security is , but it has several drawbacks that makes it almost useless in web development:
It is expensive and, at the same time, too "sloppy" and low-level (the amount of DDL code you need to write is large, and there is no framework in place to help you with it).
There is no support for "per-transaction variables" in PostgreSQL (no per-session variables as well), so if you want to pass an "acting user ID" (similar to Ent Framework VC's Principal), other than the database DDL user/role, into the query, then you can't.
PostgreSQL doesnt't support microsharding, so you basically can't recheck security against the data living in a different microshard.
In each Ent class, you need to define an explicit set of rules and determine, can a VC read that Ent (privacyLoad
), create a new Ent (privacyInsert
), update the Ent (privacyUpdate
) and delete the Ent (privacyDelete
):
When you run e.g. EntComment.loadX(vc, "123")
or any other API call, like loadBy*()
or select()
, Ent Framework runs privacyLoad
rules for each Ent.
Typically, a Rule class used in privacyLoad
is AllowIf
: it allows reading the Ent immediately as soon as the passed Predicate succeeds. There are several pre-defined Predicate classes, and you can also create your own predicates, or just pass an async boolean function; we'll discuss it a bit later.
So, the logic in the example is following:
new OutgoingEdgePointsToVC("creator_id")
: if comment.creator_id
equals to vc.principal
, then the read is immediately allowed. It means that you (vc
) are trying to read a commend which you created (its creator_id
is your user ID).
new CanReadOutgoingEdge("topic_id", EntTopic)
: if vc.principal
is able to run EntTopic.loadX(vc, comment.topic_id)
successfully, then reading of the comment is immediately allowed. This is an extremely powerful construction, the essence of Ent Framework's privacy layer: you can delegate privacy checks to other Ents in the graph. And since the engine does batching and caching aggressively, this all will be performance efficient.
Idiomatically, privacyLoad
defines access permissions in terms of graph edges reachability: typically, if there is at least one path in the graph originating from the VC and ending at the target Ent, then this VC is allowed to read the Ent.
I.e. if you run a select()
call, it will either return you all of the loaded Ents (if they all pass privacy checks) or throw a detailed error (if some of them don't). This applies to all other API calls as well.
There are several reasons for such behavior:
Performance. Ent Framework sits close to the underlying relational database. If you run a call that returns multiple Ents (e.g. select()
), you most likely want to make sure that the query matches an existing database index. So you have to encode the filtering logic in your where
condition directly, not just "bulk-load everything and then post-filter". It also applies to other aspects of fetching like pagination, limit
clause etc.
Debugging simplicity. In Meta (where privacy rules actually did filter), it was a severe pain to figure out, why some query returns you an empty (or incomplete) response. This is because privacy rules were implicitly filtering "invisible" Ents, and once the Ents are hidden, you don't even know whether they are filtered out or do not exist.
loadX()
and loadByX()
: they obviously throw EntNotReadable
error (derived from EntAccessError
base class) if the privacy checks fails.
loadNullable()
and loadByNullable()
: they will also throw EntNotReadableError
, not just return null! This greatly helps with debugging of privacy rules violations.
loadIfReadableNullable()
: this is what you want to use in an unlikely case when you really need to treat an unavailable Ent as absent. The method name is long intentionally: the best practice is to not use it too often.
As opposed to privacyLoad
, where a single succeeded rule allows the read, for privacyInsert
(as well as privacyUpdate
and privacyDelete
), all of them must pass typically.
This is because the ability to insert an Ent means that the VC has permissions to reference other Ents in all field edges. In reality, for every field edge (foreign key) defined in the Ent, there should be at least one associated Require privacy rule.
Having permissions to insert an Ent is almost always the same as having permissions to reference other Ents in its foreign key fields. If we forget to check some of the field edges, then it is possible that the user will be able to create an Ent "belonging" to someone else (by e.g. referencing someone else's ID).
The logic in the example above:
new Require(new OutgoingEdgePointsToVC("creator_id"))
: it is required that the value of comment.creator_id
is equal to vc.principal
. I.e. you can only reference yourself as a creator of the just inserted comment.
new Require(new CanReadOutgoingEdge("topic_id", EntTopic))
: it is required that, to create a comment on some topic, you must have at least read access to that topic. I.e. you can create comments on someone else's topics too, as soon as you can read those topics.
Notice that here we again use delegation: instead of introducing complicated boilerplate in comments privacy rules, we say: "I fully trust the way how privacy is implemented at EntTopic, and I don't want to know details about it at EntComment level". Basically, you build a chain of trust.
privacyUpdate/Delete
rules are similar to privacyInsert
, but they are checked by update*()
and delete*()
calls correspondingly.
If there is no privacyUpdate
block defined, then the rules are inherited from privacyInsert
array.
If there is no privacyDelete
block mentioned in the configuration, then Ent Framework uses privacyUpdate
rules for it. (And if there are no privacyUpdate
rules, then privacyInsert
).
Item in privacyLoad/Insert/Update/Delete
arrays are called a Rules. There are several built-in rules:
new AllowIf(predicate)
: if predicate
resolves to true and doesn't throw, allows the access immediately, without checking the next rules. Commonly, AllowIf
is used in privacyLoad
rules. It checks that there is at least one path in the graph originating at the user denoted by the VC and ending at the target Ent. Also, you may use AllowIf
in the prefix of privacyInsert/Update/Delete
rules to e.g. allow an admin VC access the Ent early, without checking all other rules.
new Require(predicate)
: if predicate
resolves to true and doesn't throw, tells Ent Framework to go to the next rule in the array to continue. If that was the last Require
rule in the array, allows access. This rule is commonly used in privacyInsert/Update/Delete
blocks, where the goal is to insure that all rules succeed.
new DenyIf(predicate)
: if predicate
returns true or throws an error, then the access is immediately rejected. This rule is rarely useful, but you can try to utilize it for ealy denial of access in any of the privacy arrays.
Predicate is like a function which accepts an acting VC and a database row. It returns true/false or throws an error.
The simplest way to define a predicate is exactly that, pass it as an async function:
Notice that we gave this function an inline name, CommentIsInPublicTopic
. If the predicate returns false or throws an error, that name will be used as a part of the error message. Of course we could just use an anonymous lambda (like async (vc) => {}
), but if we did so and the predicate returned false, then the error won't be much descriptive.
Here, row
is strongly-typed: you can use Ent data fields. It is not an Ent instance though, which is currently a TypeScript limitation: you can't self-reference a class in its mixin.
You can also define preticates as classes, to make them more friendly for debugging. In fact, Ent Framework's built-in predicates are implemented as classes.
As an example, let's see how a built-in predicate CanReadOutgoingEdge
works:
Each predicate class must be defined with implements Predicate
which requires the method check(vc, row)
to be implemented, as well as the name
property to exist.
In the class constructor, you accept any predicate configuration parameters and build a more descriptive name
for the predicate instance than just the predicate name.
And in check()
method, you implement your predicate's logic, the same way as you would do it in a functional predicate.
This is the simplest possible predicate, since it always returns true. It is useful when you want to create an Ent class which can be read by anyone.
Checks that ent[field]
is equal to vc.principal
. This is useful for fields like created_by
or user_id
or some similar cases, when you want to make sure that the VC's acting user is mentioned in the Ent field to make this field readable (or writable).
Delegates the privacy check to another Ent Class (ToEntClass
) considering that toEnt.id
is equal to ent[field]
. Sounds complicated, but in proactice it means the the VC has permissions to read another Ent that is parent to the current Ent, and is pointed by field
. A good example is a predicate on EntComment: privacyLoad: [new CanReadOutgoindEdge("topic_id", EntTopic)]
means that, to read this comment, the VC must be able to read its parent topic.
Similar to CanReadOutgoingEdge
above, but delegates the check to the parent Ent's privacyUpdate
rules.
Same as CanUpdateOutgoingEdge
, but for privacyDelete
delegation to the parent Ent.
Checks that there is a child Ent in the graph (EntEdge
) that points to both vc.principal
and to our current Ent. In other words, checks that there is a direct junction Ent sitting in between the VC and our current Ent. Optionally, you can provide an entEdgeFilter
callback which is fed with that junction Ent (of EntEdge
class) and should return true or false for filtering purposes.
Imagine you have EntUser
and EntOrganization
Ents, and also EntEmployment
junction Ent with (organization_id, user_id)
field edges (foreign keys). You want to check that some EntOrganization
is readable by a VC:
You use IncomingEdgeFromVCExists
just once in EntOrganization
, and then for all other children Ents, you delegate permission checks to their parent organization, using OutgoingEdgePointsToVC
typically.
This is a composite predicate, allowing to call other predicates in pallel. It returns true if any of the predicates returned true and no predicates threw an error.
Notice that you likely don't need this predicate when working with privacyLoad
, since it's typically a chain of AllowIf
rules. The AllowIf
rule already works in an "or-fashion". But for privacyUpdate/Delete
rules, the Or
predicate may be useful (Require
rule is "and-ish" on its nature).
This predicate returns true if there is flavor of a particular class added to the acting VC.
A very common case is to define your own VCAdmin
flavor which is added to a VC very early in the request cycle with vc = vc.withFlavor(new VCAdmin())
, when the corresponding user is an admin and can see any data in the database. Then, in privacyLoad/Insert/Update/Delete
of the Ent classes, you can add new AllowIf(new VCHasFlavor(VCAdmin))
to allow an admin to read that Ent unconditionally.
As opposed to , privacy rules do not post-filter the loaded Ents. They only recheck and throw.
Let's consider a common example: an Ent class with is_archived
boolean field. You obviously want the archived Ents to fail the privacy checks of a regular VC. In Ent Framework, it is not enough: you also have to modify your select()
calls to explicitly mention is_archived: false
, otherwise your queries will start throwing EntNotReadableError
when trying to load an archived Ent. Add a static helper method to your Ent class if you don't want to repear is_archived
over and over again. (BTW, to still enable archived Ents reading, you may create a VCReadArchive
.)
The way privacy rules interact with and API calls is following:
For convenience, Ent Framework already includes some of the most useful predicates. This set is constantly growing, so check for the most up-to-date list.
will be discussed later in details. For now, we can just mentioned that it's some kind of a "flag" which can be added to a VC instance for later rechecking or to carry some auxiliary information (more precisely, you can derive a new VC with a flavor added to it, since VC itself is an immutable object).