ditto icon indicating copy to clipboard operation
ditto copied to clipboard

Introduce policy decision API

Open vhdirk opened this issue 2 years ago • 6 comments

For our UI, we'd like an endpoint that we can call to check if we should enable/disable certain elements based on policies. As briefly discussed on gitter with @thjaeckle, that endpoint could look something like:

POST /api/2/policies/{policyId}/actions/checkPermissions with a payload like

{
  "resource": "thing:/features/X/properties/Y", 
  "id": "org.eclipse.ditto:mythingId",
  "permissions": [ "READ" ],
 }

The response could be something like

[
   {
      "permission": "READ",
      "allowed": true/false
   }
]

That would require you to have the policyId at hand, potentially having to fetch the Thing prior to using this api. It does make it generic to use for any kind of resource (thing, policy, messages)

vhdirk avatar Aug 03 '21 12:08 vhdirk

@vhdirk I don't get why you added the "id" of the Thing in the payload. You already define by addressing the policy via the "policyId" in the HTTP path, which policy to check. And as inside of the policy there is no "link" to a special Thing ID, this makes no sense (the relation is the other way around, the thing points to the policy).

Basically what I think you need:

  • You have a Thing "namespace:thing-1" which uses policy "namespace:policy-1"
  • You check if you have "READ" permission on "namespace:thing-1" by doing POST /api/2/policies/namespace:policy-1/actions/checkPermissions
    •  {
         "resource": "thing:/", 
         "permissions": [ "READ" ]
        }
      
  • Response could IMO be much simpler:
    • true|false
      

By using the "actions", one would however require to have EXECUTE permission for policy:/actions/checkPermissions - I am not certain if that is a good idea or if that API endpoint should be callable by every authenticated user instead.

thjaeckle avatar Aug 03 '21 12:08 thjaeckle

I forgot to update the payload after I added the policy id as a pathparam :) I was pondering with the idea of not requiring any id in request url, so that you did not need to first fetch the policyId from the resource itself. You could just give it a thingId, policyId or messageId and it would fetch the policyId internally.

As far as the response goes; the idea was that you could test multiple permissions independently. But there's not that many combinations to be made with permissions, so I think the simple boolean response would be fine.

vhdirk avatar Aug 03 '21 12:08 vhdirk

I was pondering with the idea of not requiring any id in request url, so that you did not need to first fetch the policyId from the resource itself. You could just give it a thingId, policyId or messageId and it would fetch the policyId internally.

That won't work as the Ditto "policies" service/persistence does not track "backward relations" and architecturally we don't want it to.

Solution could be simple: Always make "policyId" and "thingId" the same, then you won't need to look up the "policyId" before checking the policy permissions.

thjaeckle avatar Aug 03 '21 13:08 thjaeckle

Since frontends usually have to check for multiple paths at once to reduce the number of requests per page load the endpoint should accept multiple requests in one. E.g. with a payload like:

[
  {
    "resource": "thing:/features/X/properties/Y",
    "permissions": [ "READ" ]
  },
  {
    "resource": "thing:/features/Z",
    "permissions": [ "WRITE" ]
  }
]

And a result payload like:

[
  {
    "resource": "thing:/features/X/properties/Y",
    "permissions": [ "READ" ],
    "match": true
  },
  {
    "resource": "thing:/features/Z",
    "permissions": [ "READ" ],
    "match": false
  }
]

Or simplified:

{
  "thing:/features/X/properties/Y": true,
  "thing:/features/Z": false
}

Maybe it's also useful if I can call that permission check action on a thing and ditto then forwards this to the policy check action with the corresponding thingId. That way I don't have to find out the policyId of a thing first.

When retrieving a thing or feature one could also supply a path parameter to request deviating permissions so if I GET a feature and I'm interested if there are properties on that feature I can't READ or WRITE then I'd like to know that. Similar to the extraFields parameter. Actually maybe only if I can't write, because if I can't read then I don't get it returned anyway.

w4tsn avatar Sep 02 '21 06:09 w4tsn

Thinking about this feature request again ..

  • a user might not have the permission to even READ the "policyId" of a thing
  • but still, a UI would need to query Ditto which "permissions" a provided login/JWT has for a specific thing
  • so, maybe (thinking out loud) this would have to be an endpoint which is neither "thing" nor "policy" related - similar to the whoami endpoint

Idea (building on input provided by @w4tsn and @vhdirk):

POST /api/2/checkPermissions

Payload:

[
  {
    "resource": "thing:/features/lamp/properties/on",
    "entityId": "org.eclipse.ditto:some-thing-1",
    "hasPermissions": [ "READ" ]
  },
  {
    "resource": "message:/features/lamp/inbox/toggle",
    "entityId": "org.eclipse.ditto:some-thing-1",
    "hasPermissions": [ "WRITE" ]
  },
  {
    "resource": "policy:/",
    "entityId": "org.eclipse.ditto:some-policy-1",
    "hasPermissions": [ "READ", "WRITE" ]
  }
]

The response payload would either just be:

[
  true,
  true,
  false
]

Or it would include the whole "input" (maybe optionally with a "verbose" flag).

And an alternative to that (as JsonArrays are often problematic to handle, using indices, etc.):

{
  "lamp_reader": {
    "resource": "thing:/features/lamp/properties/on",
    "entityId": "org.eclipse.ditto:some-thing-1",
    "hasPermissions": [ "READ" ]
  },
  "lamp_toggler": {
    "resource": "message:/features/lamp/inbox/toggle",
    "entityId": "org.eclipse.ditto:some-thing-1",
    "hasPermissions": [ "WRITE" ]
  },
  "policy_admin": {
    "resource": "policy:/",
    "entityId": "org.eclipse.ditto:some-policy-1",
    "hasPermissions": [ "READ", "WRITE" ]
  }
}

Which could (by default) just return a response like:

{
  "lamp_reader": true,
  "lamp_toggler": true,
  "policy_admin": false
}

Or it would include the whole "input" (maybe optionally with a "verbose" flag).

thjaeckle avatar Nov 21 '23 15:11 thjaeckle

Ideas:

  • optional "verbose" mode, responding with requested "input" on "resources": POST /api/2/checkPermissions?verbose=true
  • "entityId" could also be provided via the HTTP path or query params .. to reduce payload for large permission check requests: POST /api/2/checkPermissions?entityId=<entityId>

thjaeckle avatar Nov 21 '23 15:11 thjaeckle