fluree / core

Fluree releases and public bug reports
0 stars 0 forks source link

Implement "query" fn for policy evaluation #37

Open aaj3f opened 8 months ago

aaj3f commented 8 months ago

Description

While we may ultimately plan to implement a variety of logical functions to evaluate whether or not a piece of data can be accessed by queries/txns, the most generic and useful fn would be akin to a query

i.e. if this query returns a result then the access is granted, if not it is not granted

Consider, for example, how f:equals works. If the path described on f:equals resolves to an entity that is the entity in question for a query/txn, then the policy-described action is allowed, if not, it is disallowed

:f/path  :schema/ssn
:f/allow [{:id           :ex/ssnViewRule
                :f/targetRole :ex/userRole
                :f/action     [:f/view]
                :f/equals     {:list [:f/$identity :ex/user]}}]}

This is effectively a (sub)query in evaluating access. Other possible fns we could eventually add would also likely be subqueries to evaluate access. A significant first step towards data-centric, and data-state-defined access policy evaluation would be to allow the end user to compose their own queries which, if any true-coercible value is returned, would fail, and on any false-coercible results, would pass, e.g.

{
        "@type": ["f:Policy"],
        "f:targetNode": {"@id" : "f:allNodes"},
        "f:allow": [
            {
                "@id": "ex:publicViewAllow",
                "f:targetRole": {"@id" : "ex:publicRole"},
                "f:action": [{"@id" : "f:view"}],
                "f:query": {
                    "selectOne": "?isTrue",
                    "where": [
                         { "optional": [["#(sid)", "ex:confidential", "?isTrue"]] }
                    ]
                 }
            }
        ]
}

Note: This is actually the exact opposite of what was done in v2. The behavior used to be that if a true-coercible value was returned, the operation would pass and be valid. We've amended this to comply with SHACL sparql-based-constraints. They express, very clearly, that true-coercible results from a query should indicate an INVALID scenario where the operation should FAIL: https://www.w3.org/TR/shacl/#sparql-constraints-validation

cap10morgan commented 8 months ago

@aaj3f In the explanation for the true => fail / false => pass behavior, are you referring to this language in the blue "textual definition" box in the spec?

There is one validation result for each solution that does not have true as the binding for the variable failure. These validation results must have the property values explained in 5.3.2 Mapping of Solution Bindings to Result Properties. A failure must be produced if and only if one of the solutions has true as the binding for failure.

If so, I'm not reading it the same way. It sounds like it's specifically talking about the value of a failure variable.

aaj3f commented 8 months ago

Here's an example: https://www.w3.org/TR/shacl/#sparql-constraints-example

The query returns true for ex:invalidCountry and so the validation fails for that record

aaj3f commented 8 months ago

If instead of (!isLiteral(?value) || !langMatches(lang(?value), "de") they had had (isLiteral(?value) && langMatches(lang(?value), "de"), then it would have returned true for the valid/passing entity, ex:ValidCountry. I'm fairly new to this SHACL/SPARQL part of the spec, so I may be wrong, but I do believe that they mean "if the SHACL query would return something true (or true-coercible), then it should fail validation"

cap10morgan commented 8 months ago

OK, thanks, I think I understand now. @mpoffald helpfully reminded me that in most other contexts SHACL is used as a tool to query for violations of the constraints, so that's why this is kind of implicitly designed that way. Except for ASK-based validators which work the opposite way, but I think that's out of scope for this issue.

mpoffald commented 8 months ago

Just to fully clarify, what is the expected behavior of this policy?

{
        "@type": ["f:Policy"],
        "f:targetNode": {"@id" : "f:allNodes"},
        "f:allow": [
            {
                "@id": "ex:publicViewAllow",
                "f:targetRole": {"@id" : "ex:publicRole"},
                "f:action": [{"@id" : "f:view"}],
                "f:query": {
                    "selectOne": "?isTrue",
                    "where": [
                         { "optional": [["#(sid)", "ex:confidential", "?isTrue"]] }
                    ]
                 }
            }
        ]
}

In generaI I get the idea of querying for violations a la SHACL, but I think I'm confused by the key f:allow here. It seems like if the underlying query returns data that is forbidden, it should be under f:disallow ? unless I'm also misunderstanding

aaj3f commented 8 months ago
{
        "@type": ["f:Policy"],
        "f:targetNode": {"@id" : "f:allNodes"},
        "f:allow": [
            {
                "@id": "ex:publicViewAllow",
                "f:targetRole": {"@id" : "ex:publicRole"},
                "f:action": [{"@id" : "f:view"}],
                "f:query": {
                    "selectOne": "?isTrue",
                    "where": [
                         { "optional": [["#(sid)", "ex:confidential", "?isTrue"]] }
                    ]
                 }
            }
        ]
}

Looking closer at this, I think the optional is probably unnecessary. But the behavior would be that if the node in question has a value on ?isTrue (in theory we'd expect that to be a boolean value), that value is returned (and because of selectOne it's returned as the value itself, not as [value]). If that value is true or true coercible, then the behavior is disallowed.

So if I have [ex:andrew, ex:confidential, true], the query returns true, the user in ex:publicRole cannot view this node And if I have [ex:andrew, ex:confidential, false] (or if no fact exists on [ex:andrew, ex:confidential]), then the value returned is not true-coercible, and the view action is permitted