HydraCG / Specifications

Specifications created by the Hydra W3C Community Group
Other
138 stars 26 forks source link

Retracting operations #241

Open alien-mcl opened 2 years ago

alien-mcl commented 2 years ago

It seems that hydra is missing one final feature in it's core - retracting (or forbidding) operations.

While allowing operations is cool, there are situations when cancelling those allowances would come in handy.

Imagine a situation, when there is an operation allowed on a specific class of resources, but due to some specific circumstances, a very specific resource of that given class may be in a state that would forbid an operation from executing. API should have a way of telling it.

Proposed solution - adding a new resource level predicate forbiddenOperation which could take precedence over already existing operation (also on resource level) and supportedOperation on API documentation level. The only issue I see here is how to identify an operation being forbidden.

The easiest would be just to assign an operation previously declared an IRI which could then be pointed by the forbiddenOperation.

Example

GET /api?documentation

{
  "supportedClass": "api:User",
  "api:User": {
    "supportedOperation": {
      "@id": "api:deleteUser",
      "method": "DELETE"
    }
  }
}
GET /api/user/1

{
  "forbiddenOperation": "api:deleteUser"
}

The example above declares an operation deleteUser for all resources of class User, but a very resource of /api/user/1 disallows that operation due to i.e. fact this user is still on the registration stage and business rules does not allow to delete it prematurely.

Feel free to deliberate on that feature as I'd love to add required specification changes to have it in the core.

tpluscode commented 2 years ago

I cannot quickly single the precise messages on the mailing list but I'm pretty sure this would have been proposed in the past and was always a debated topic. The closest thread is Modeling permissions with Hydra.

There is much to go through and I still cannot make up my mind. How do other hypermedia APIs approach this problem? @asbjornu @serialseb

alien-mcl commented 2 years ago

I knew you'd end up with that. But the example I've used has nothing to do with permissions. There are no claims user can get to perform the mentioned operation - it's a matter of some business logic behind that very resource and it's state.

I don't want to model permissions - I just want to have means of saying no, we can't instead of usual yes, we can.

asbjornu commented 2 years ago

The reason operations are retracted or overridden – whether it's due to business logic or the lack of permissions – doesn't really matter, imho. My stance on this is still the same as expressed in the e-mail thread. I don't think other hypermedia formats have an explicit solution to this; the common thing to do is to embed all available operations in the response and then treat all missing operations as unavailable (for whatever reason; business logic, permissions or whatnot).

serialseb commented 2 years ago

Well, I tend to document all available operations in ApiDocumentation, give them a @type of [Opereation, xxx] where xxx tends to be a schema:Action-derived. I then only enable each operation in a UX if it is in the operation property inline in the response for that resource, and add the ones that were not in ApiDocumentation as enabled. It makes sense in that respect.

The solution is imperfect, of course, because a lot of customers will look at the static documentation to code their Api, not the inline operation. I do think there's a more generic way that one could annotate operations within a specific context, one that could apply to templates, but as this is for hypermedia clients, I would want to inline some state that would allow a customer to resolve any reason for which this would not be allowed.

The two starting points i'd start from is the Allow http header that is additive, which tends to be solved by the first approach above, but the second could maybe be generalised with the equivalent of the enabled property on html form submit elements.

Could we support an enabled: false of some kind on hydra:Operation, and an additional field describing the current state of the resource? This would have the advantage of taking an external ApiDocumentation, say, a standard for banking, and overlaying your own ApiDocumentation on top, or inline an availability, one that could map to an enumeration a la schema.org?

My thinking would be availability: Available | NotImplemented | Forbidden | Unauthorized | ConflictsWithCurrentStateOfResource | myapi:DisabledByAdministrator, maybe making this a class that includes the enabled, and a description that could be useful in rendering user experience?

Thinking out loud here, so sorry if the proposal is a bit vague

alien-mcl commented 2 years ago

The reason operations are retracted or overridden – whether it's due to business logic or the lack of permissions – doesn't really matter, imho

That's my point of view as well as I do not wan't to model permissions.

Well, I tend to document all available operations in ApiDocumentation ... I then only enable each operation in a UX if it is in the operation property inline in the response for that resource

This is against the specification. All operations declared in API Documentation and inlined are complementary - there is neither mechanism of overriding nor the hiding of previously declared operations.

My thinking would be availability: Available | NotImplemented | Forbidden | Unauthorized | ConflictsWithCurrentStateOfResource | myapi:DisabledByAdministrator, maybe making this a class that includes the enabled, and a description that could be useful in rendering user experience?

I like the idea of providing an availability. While it should not matter from the client point of view (server can always say i.e. Forbidden), it might be useful to display some kind of explanation for humans why the button is greyed out.

Example:

GET /api?documentation

{
  "supportedClass": "api:User",
  "api:User": {
    "supportedOperation": {
      "@id": "api:deleteUser",
      "method": "DELETE"
    }
  }
}

GET /api/user/1

[{
  "@id": "api:deleteUser",
  "availability": "hydra:Forbidden"
}]
tpluscode commented 2 years ago

Indeed, I remember the discussions we had, and it seemed very practical to not only mark an operation as retracted but also providing the reason.

Only how 'bout we make this an extension and not core? My thinking is that there are multiple ways to go about it and it will be challenging to find common ground. Maybe we could agree to keep the core as-is, with the additive semantics. A hypothetical "negative supported operations extension" would be easier to manage with more flexibility.

This way we don't put additional requirements on minimal compliant clients (worst case, they will get 401/403/405) and at the same time such an extension might provide more that one alternative for clients and servers to achieve the desired functionality. Or leaving it up to a particular server to choose its own path

tpluscode commented 2 years ago

Off the top of my head, the actual operation being retracted has to be directly associated with a specific resource.

And maybe we could reuse hydra:operation and add a class of unsupported operations?

GET /article/1

{
  "@id": "/article/1",
  "@type": "schema:Article",
  "api:status": "api:published",
  "operation": {
    "@type": ["hydra:UnsupportedOperation", "schema:RemoveAction"],
    "rdfs:comment": "Published articles cannot be removed"
  },
  "schema:primaryImageOfPage": {
    "schema:contentUrl": "/image/article-1", 
    "operation": {
      "@type": ["hydra:UnsupportedOperation", "schema:ReplaceAction"],
      "rdfs:comment": "Insufficient permissions"
    }
  }
}

This way, a client implemented to go through the inline operations first would gather those unsupported and exclude any matches supported by classes (schema:Articles) and properties (schema:Article schema:primaryImageOfPage).

alien-mcl commented 2 years ago

Only how 'bout we make this an extension and not core?

I'm keen to have it in the core - it's better to have extensions to take care of something that is out of scope of hydra. Hypermedia controls telling what can be done and what cannot be done feels to fall in that scope.

...retracted has to be directly associated with a specific resource.

Not really. I can imagine there is a supported operation for resources of class Collection, but resources of class ReadOnlyCollection would have that operation disabled on the API documentation level (I know that example is a long shot ;))

And maybe we could reuse hydra:operation and add a class of unsupported operations?

We could, but it seems not enough. Marking operations with logical behavior like schema:RemoveAction is outside of the spec and seems not be unique enough (you could have several operations on some resource marked the same way, but i.e. expecting different resources.

tpluscode commented 2 years ago

Fair enough, let's do core if others will agree.

Not really. I can imagine there is a supported operation for resources of class Collection, but resources of class ReadOnlyCollection would have that operation disabled on the API documentation level (I know that example is a long shot ;))

This is exactly the reason I opted for an extension. Either we make it as simple as possible, or not core. Maybe more verbose, but retracting surgically on individual resources is easy to reason about. This is not OOP, where I gather a term like ReadOnlyCollection would originate.

And this is the only sane way I can propose to retract operations supported by properties. This concept is difficult enough on its own...

Marking operations with logical behavior like schema:RemoveAction is outside of the spec and seems not be unique enough

How is it outside of the spec? Not unique? Yes, you would introduce custom operation/action types for more precise semantics. For example

:Article
  a hydra:Class ;
  hydra:supportedOperation [ a schema:UpdateAction , api:SendArticleToReviewerQueue ] .

# This is by no means ambiguous ;)
</article/a> hydra:operation [ a hydra:UnsupportedOperation , api:SendArticleToReviewerQueue ] .
tpluscode commented 2 years ago

And I also support your example, where you retracted by the operation's URI.

# ApiDocumentation
:Article
  a hydra:Class ;
  hydra:supportedOperation :SendArticleToReviewerQueue .
# Resource
</article/a> hydra:operation :SendArticleToReviewerQueue .

:SendArticleToReviewerQueue a hydra:UnsupportedOperation .

However, many operations would in fact be blank nodes, hence using the type instead

asbjornu commented 2 years ago

+1 to have this in Core.

alien-mcl commented 2 years ago

This is not OOP, where I gather a term like ReadOnlyCollection would originate.

I know, but nothing better came up.

Not unique? Yes, you would introduce custom operation/action types for more precise semantics.

Imagine that:

# ApiDocumentation

api:Basket a hydra:Class;
  hydra:supportedOperation [
    a hydra:Operation, api:AddToBasket;
    hydra:expects api:Product
  ],[
    a hydra:Operation, api:AddToBasket;
    hydra:expects api:Service
  ]
# Resources
</services/whatever> a api:Service;
  hydra:operation [
    a hydra:Operation, api:AddToBasket;
    hydra:expects api:Service;
    hydra:availability hydra:Forbidden
  ]

User may have a Service (this class is disjoined from Product as i.e. in GoodRelations) being configured, but it cannot be added to the basket yet - and in the example above providing a type is not enough. It will be unique enough with expected type, but it's getting complex. I'd stay at operation's Iri.

tpluscode commented 2 years ago

I'd stay at operation's Iri.

giphy

Like I said, most of my operations are blank nodes. I would not like to change that "only" to be able to retract operations.

Also, maybe the semantics could be to "retract an operation which shares all properties" or exact IRI. So, given your example, the client would also use the hydra:expects and retract only the operation which has the same value...

tpluscode commented 2 years ago

This way of applying a match to retract will make it possible to retract operations with custom meta properties an API could use to describe operations, not only this defined by Hydra

serialseb commented 2 years ago

I do believe that the fact the spec doesn't guide in where things go between operation and ApiDocument makes me bleieve that something should be touched upon in the spec, which would prevent people like me from building enabled / disabled as I did :) That said, looking over to other api documentation tools, having an operation have an Id is quite common, so i wouldn't mind much leveraging this to make operation identification more clear, if it enables those scenarios.

So from me, I'm happy for this to be in core in it's simplest form, i'd be happy with additional information about the availability of an operation to be in an extension so it can bake longer until we learn enough about the scenarios, and i'd be happy with IRIs for operations.