Combine logical expressions while prioritizing expressions - math

I'm in the process of creating a complex authorization module for a system which will be based on the specification pattern. Access rules are defined as followed:
An operationName defines to which operation the rule applies to.
A SubjectSpecification defines wheter the rule should apply to a specific Subject or not. E.g. the following would match any subject within the division A.
subjectSpec = new MyAppSubjectSpecification(division:
Division.SOME_DIVISION)
An AccessSpecification defines the actual rule expression.
An AccessType defines wheter the matching rule grants or deny access.
A RulePriorityStrategy that defines how rules shall be inherited.
For the question, let's say that our application's Subject has the following structure {divisionId, sectionId, groupId, userId } and that the RulePriority states that the more specific the SubjectSpecification is then the higher it's priority is.
For instance, a rule targeting a specific userId would be prioritized over a rule targeting the division to which that user belongs to.
Now, here's the real challenge which I'm not sure how to solve yet. I want rules to be inherited in a way that high priority rules inherit from the lowest priority rules, while having their own condition override parts of the lowest priority rules which are conflicting. Please also note that operations are denied by default if there are no matching grant rule.
For instance, with the following rules:
- Grant OperationA to all where (A && B) || C
- Grant OperationA to division A where D
- Deny Operation to user xyz where B && E
Here I'd expect user xyz (which belongs to division A) to be allowed when C || D || (A && B && !E).
The way I was planning to use the final inherited grant and allow rules is as follow:
canPerformOperation = grantSpec.isSatisfiedBy(...) and not (denySpec.isSatisfiedBy(...))
In this case it seems that combining in the following way would work, but it does not in all scenarios (e.g. when allow must override denial).
GR = Grant rule
DN = Deny rule
allowed = (GRn || GRn+1 ... || GRn+x)) && !(DRn || DRn+1 ... || DRn+x)
allowed = ((A && B) || C) || D) && !(B && E)
I've made a bit of research and I'm sure this is standard academic knowledge of logical expressions, but I couldn't find a standard algorithm that solves the issue somehow and I'm sure coming up with a solution will take me quite a lot of time or will not be efficient.

Related

Security rules dont cascade like the docs said

The security rules dont cascade, like the docs says.
This picture demonstrates the result of an authorized read request to path /foo/baz/bar/ done with the simulator.
The Firebase Docs says this (code example is relevant to the docs):
{
"rules": {
"foo": {
".read": true,
".write": false
}
}
}
.read and .write rules cascade, so this ruleset grants read access to
any data at path /foo/ as well as any deeper paths such as
/foo/bar/baz. Note that .read and .write rules shallower in the
database override deeper rules, so read access to /foo/bar/baz would
still be granted in this example even if a rule at the path
/foo/bar/baz evaluated to false.
Why do i get the opposite effect?
Allowing access cascades, denying access does not. If denying access had the same cascading effect, rules would become verbose since you would have to explicitly exclude every part of the database you don't want affected even when denying.
Think of rules as a big or statement -- it goes through each matching rule one by one until it finds a true:
rule1 || rule2 || rule3 || rule4 ...

Firestore security rules - read count in a batch

If in a batch I update documents A and B and the rule for A does a getAfter(B) and the rule for B does a getAfter(A), am I charged with 2 reads for these or not? As they are part of the batch anyway.
Example rules:
match /collA/{docAid} {
allow update: if getAfter(/databases/$(database)/documents/collA/${docAid}/collB/{request.resource.data.lastdocBidupdated}).data.timestamp == request.time
&& ...
}
match /collA/{docAid}/collB/{docBid} {
allow update: if getAfter(/databases/$(database)/documents/collA/${docAid}).data.timestamp == request.time
&& getAfter(/databases/$(database)/documents/collA/${docAid}).data.lastdocBidupdated == docBid
&& ...
}
So are these 2 reads, 1 per rule, or no reads at all?
firebaser here
I had to check with our team for this. The first feedback is that it doesn't count against the maximum number of calls you can make in a single security rule evaluation run.
So the thinking is that it likely also won't count against documents read, since it doesn't actually read the document. That said: I'm asking around a bit more to see if I can get this confirmed, so hold on tight.
Are you using two different documents?
If it is the case, then two reads will be performed.

Firebase Security rules comparison logic

I have a question about how cloud firestore security rules' logic works:
Lets say I have rule such as this:
allow read: if (auth.token.user === true && request.query.limit < 100 && uidInDocument()) || auth.token.admin === true && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.name == "foo";
Now, my question here is if the first part of the or statement is true, does that mean that the second part doesn't execute? I'm thinking about this as I don't want to be incurring reads unless it is necessary to do so, in this case if the user is an admin
Thanks in advance.
Boolean expression in security rules evaluate left to right and short circuit just like pretty much all other programming languages. If you have a series of logical AND operations, and the first one is false, then the entire rule is finished, and access is rejected.
Please read the documentation for more information.

Firestore rules, using short circuit to squeeze out reads

Are these two Firestore rules different at all in the number of reads that they spend from my quota? Note that isWebAdmin() does an exists(), which eats away from my read quota.
// example 1
match /companies/{company} {
// rule 1
allow list, write: if isWebAdmin();
// rule 2
allow get: if isInCompany(company)
// when isInCompany is true, this is short-circuited away
|| isWebAdmin();
}
vs.
// example 2
match /companies/{company} {
// rule 1
allow read, write: if isWebAdmin();
// rule 2
allow get: if isInCompany(company);
}
Here is my (possibly faulty) reasoning: For most get requests isInCompany(company) will be true and isWebAdmin() will be false. Therefore, in example 2, even though the user is authorized to get with rule 2, rule 1 will also execute because get is also a read. So, while trying to give the admin access, I'm spending more reads for regular users who have access.
In example 1, I separate out get and list and treat them separately. In get requests, it will not run rule 1 at all. When running rule 2, since isInCompany(company) is true, isWebAdmin() won't execute because of short circuiting. So, in the common case I saved a read by avoiding calling isWebAdmin().
Is this correct? If so, simply slapping admin privileges adds gets for each user's regular operation. I find this a bit inconvenient. I guess if this is not the case, we should be billed by only the "effective" rule, not everything that was tested. Is that the case instead?
With Firebase security rules, boolean expressions do short circuit, which is a valid way of optimizing the costs of your rules. Use the more granular rules in example 1 for that.

Having consistency during multi path updates when the paths are not deterministic and are variable

I need help in a scenario when we do multipath updates to a fan-out data. When we calculate the number of paths and then update, in between that, if a new path is added somewhere, the data would be inconsistent in the newly added path.
For example below is the data of blog posts. The posts can be tagged by multiple terms like “tag1”, “tag2”. In order to find how many posts are tagged with a specific tag I can fanout the posts data to the tags path path as well:
/posts/postid1:{“Title”:”Title 1”, “body”: “About Firebase”, “tags”: {“tag1:true, “tag2”: true}}
/tags/tag1/postid1: {“Title”:”Title 1”, “body”: “About Firebase”}
/tags/tag2/postid1: {“Title”:”Title 1”, “body”: “About Firebase”}
Now consider concurrently,
1a) that User1 wants to modify title of postid1 and he builds following multi-path update:
/posts/postid1/Title : “Title 1 modified”
/tags/tag1/postid1/Title : “Title 1 modified”
/tags/tag2/postid1/Title : “Title 1 modified”
1b) At the same time User2 wants to add tag3 to the postid1 and build following multi-path update:
/posts/postid1/tags : {“tag1:true, “tag2”: true, “tag3”: true}
/tags/tag3/postid1: {“Title”:”Title 1”, “body”: “About Firebase”}
So apparently both updates can succeed one after other and we can have tags/tag3/postid1 data out of sync as it has old title.
I can think of security rules to handle this but then not sure if this is correct or will work.
Like we can have updatedAt and lastUpdatedAt fields and we have check if we are updating our own version of post that we read:
posts":{
"$postid":{
".write":true,
".read":true,
".validate": "
newData.hasChildren(['userId', 'updatedAt', 'lastUpdated', 'Title']) && (
!data.exists() ||
data.child('updatedAt').val() === newData.child('lastUpdated').val())"
}
}
Also for tags we do not want to check that again and we can check if /tags/$tag/$postid/updatedAt is same as /posts/$postid/updatedAt.
"tags":{
"$tag":{
"$postid":{
".write":true,
".read":true,
".validate": "
newData.hasChildren(['userId', 'updatedAt', 'lastUpdated', 'Title']) && (
newData.child('updatedAt').val() === root.child('posts').child('$postid').val().child('updatedAt').val())”
}
}
}
By this “/posts/$postid” has concurrency control in it and users can write their own reads
Also /posts/$postid” becomes source of truth and rest other fan-out paths check if updatedAt fields matches with it the primary source of truth path.
Will this bring in consistency or there are still problems? Or can bring performance down when done at scale?
Are multi path updates and rules atomic together by that I mean a rule or both rules are evaluated separately in isolation for multi path updates like 1a and 1b above?
Unfortunately, Firebase does not provide any guarantees, or mechanisms, to provide the level of determinism you're looking for. I have had the best luck front-ending such updates with an API stack (GCF and Lambda are both very easy, server-less methods of doing this). The updates can be made in that layer, and even serialized if absolutely necessary. But there isn't a safe way to do this in Firebase itself.
There are numerous "hack" options you could apply. You could, for example, have a simple lock mechanism using a dedicated collection for tracking write locks. Clients could post to a lock collection, then verify that their key was the only member of that collection, before performing a write. But I hope you'll agree with me that such cooperative systems have too many potential edge cases, potential security issues, and so on. In Firebase, it is best to design such that this component is not a requirement in the first place.

Resources