-
-
Notifications
You must be signed in to change notification settings - Fork 97
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a Middlewares
algebra that allows to modify Endpoint
#847
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @mleclercq for this work!
This is interesting and this solves several issues. Please find my comments below.
@@ -14,6 +14,7 @@ import endpoints4s.algebra.Documentation | |||
|
|||
/** @group interpreters | |||
*/ | |||
@deprecated("Use AuthenticatedEndpointsServer instead", "1.5.0") | |||
trait BasicAuthentication extends algebra.BasicAuthentication with EndpointsWithCustomErrors { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible to keep BasicAuthentication
not deprecated, but to implement it with Middlewares
?
//#endpoint-type | ||
case class Endpoint[A, B](request: Request[A], response: Response[B]) | ||
extends (A => Future[B]) | ||
//#endpoint-type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the comment automatically indented by scalafmt? Is it possible to align it with the start tag?
algebras/algebra/src/main/scala/endpoints4s/algebra/Middlewares.scala
Outdated
Show resolved
Hide resolved
algebras/algebra/src/main/scala/endpoints4s/algebra/Middlewares.scala
Outdated
Show resolved
Hide resolved
algebras/algebra/src/main/scala/endpoints4s/algebra/Middlewares.scala
Outdated
Show resolved
Hide resolved
if (Random.nextBoolean()) Some(()) | ||
else None // Randomly return an unauthorized | ||
case None => | ||
None // Missing credentials. always return an unauthorized |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should distinguish between returning Unauthorized
and Forbidden
.
When a client does not supply the Authorization
header, he should get an Unauthorized
response, and when he supplies invalid credentials, he should get a Forbidden
response.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not certain that the Forbidden
status code means "invalid credentials". From experience, I think most servers would return a 401 Unauthorized
status code if the client provides invalid credentials.
From https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403:
The HTTP 403 Forbidden client error status response code indicates that the server understood the request but refuses to authorize it.
This status is similar to 401, but in this case, re-authenticating will make no difference. The access is permanently forbidden and tied to the application logic, such as insufficient rights to a resource.
So I think Forbidden
reflects a lack of permission to perform the action. The user is properly authenticated but doesn't have the right to perform the action.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that’s how it works in BasicAuthentication
: when the request is processed by the server interpreter, it is rejected with an Unauthorized
response in case the credentials are missing. If the credentials are provided, the endpoint implementation can signal that the authenticated user lacked the right to access the resource by returning None
(in which case the server produces a Forbidden
response).
type Endpoint[A, B] = DocumentedEndpoint | ||
case class Endpoint[A, B]( | ||
request: Request[A], | ||
response: Response[B], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to keep the request and response types here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need it to be able to implement mapEndpointResponse
and mapEndpointRequest` required by the new algebra.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but since type Response[A] = DocumentedResponse
, the operation mapEndpointResponse
could be implemented as a no-op, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't think about that, but that makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can change the Endpoint
class to be:
case class Endpoint[A, B](
request: DocumentedRequest,
response: List[DocumentedResponse],
docs: EndpointDocs,
securityRequirements: Seq[SecurityRequirement]
)
But I don't think that mapEndpointResponse
and mapEndpointRequest
could be made as no-op as the final DocumentedEndpoint
should reflect the changes made to the request or response.
The thing is that the Request
and Response
are "sliced and dices" in the DocumentedEndpoint
so it would be pretty hard to implement mapEndpointResponse
and mapEndpointRequest
given a DocumentedEndpoint
. So instead, the Endpoint
keeps the Request
and Response
which makes the implementation of mapEndpointResponse
and mapEndpointRequest
trivial and the DocumentedEndpoint
is a lazy val
of Endpoint
so it always reflects the result of these mappings.
* | ||
* @group interpreters | ||
*/ | ||
trait AuthenticatedEndpointsServer extends AuthenticatedEndpoints { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One concern I have is that we don’t really encapsulate the fact that if the Authorization
header is missing, then the server returns an Unauthorized
response, otherwise it forwards to the underlying endpoint logic.
IIUC, in the current implementation we always call the underlying endpoint logic. I believe it would be interesting to be able to intercept requests.
Maybe we need something different from mapRequest
and mapResponse
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, a client could send a request without the Authorization
header and a giant payload and the server would tries to decode the payload and only after reject the request because the header is missing. That's kind of a security vulnerability.
I guess, the implementations of addRequestHeaders
and addRequestQueryString
could be made such that they first extract the added headers/queryString and call the extraction of the rest of the request only if the former succeeded.
Edit: What I suggested above would not solve that problem. I remember at some point I played with a partialEndpointImplementation
operation that takes a PartialFunction[A, B]
, but I guess even that wouldn't help since it still needs to extract the full A
. I'll try to see how this can be addressed...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be useful to control whether to call or not the underlying endpoint logic. A bit like what they do in Play, with the ActionFunction
hierarchy.
Thanks for making this PR @mleclercq. Can you maybe process the review comments? Otherwise I also have some time tomorrow (or otherwise next week) to continue this PR. In case you are short on time. |
Deprecate BasicAuthentication Add tests for AuthenticatedEndpoints which also tests Middlewares
@@ -95,7 +96,7 @@ trait EndpointsWithCustomErrors | |||
case x => throw InvalidHeaderDefinition(x) | |||
} | |||
|
|||
type Request[A] = A => Future[HttpResponse] | |||
type Request[A] = A => HttpRequest |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be sensible to keep the Future in this type? i.e.:
type Request[A] = A => Future[HttpRequest]
When we handle a request with authentication, the HTTP credentials are validated using a separate microservice. With the old type we could leverage the Future to perform that validation via a separate and get the necessary account data.
I realize this is kind of an edge case, but it would make migration for us easier and since the Future was already there I can imagine that others may depend on this too.
I've pushed a working branch here, the change wasn't too impactful: https://github.com/simacan/endpoints4s/tree/middleware-take-two
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've played around with this a bit and really like the added methods for mapping endpoints and adding constituents! I've left some thoughts below about the details, but in general this PR will solve #794 🎉
algebras/algebra/src/main/scala/endpoints4s/algebra/Middlewares.scala
Outdated
Show resolved
Hide resolved
mapEndpointResponse(endpoint, f) | ||
def mapDocs(f: EndpointDocs => EndpointDocs): Endpoint[A, B] = | ||
mapEndpointDocs(endpoint, f) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible to add accessors to this as well, e.g. for the statuscode, url etc.? This would enable us to implement #756 in a composable fashion
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I see how adding accessors helps with #756.
Currenlty the Endpoint
concrete types for client and server interpreters do not keep a reference to the EndpointDocs
because it isn't used in anyway. So for these interpreters, mapDocs
is a no-op. Only the openapi interpreter actually keeps a reference to EndpointDocs
. So if we add accessors in the algebra, all interpreters would need to keep the reference to EndpointDocs
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I should've been more clear. I didn't mean an accessor for EndpointDocs
, but for other parts of the endpoint, such as the request url, the response statuscode etc. I can try it out in a separate PR or even as part of #756 if that's preferred, so it doesn't hold back this PR.
2d06397
to
f2df19a
Compare
Codecov Report
@@ Coverage Diff @@
## master #847 +/- ##
==========================================
- Coverage 68.51% 67.48% -1.04%
==========================================
Files 137 139 +2
Lines 3656 3847 +191
Branches 65 61 -4
==========================================
+ Hits 2505 2596 +91
- Misses 1151 1251 +100
Continue to review full report at Codecov.
|
Sorry for the delay for responding to the comments. I had a couple of very busy weeks... I've rebased my branch on @agboom ones which includes the change proposed for akka-client (type Request[A] = A => Future[HttpRequest]). I did the following changes:
|
Thank you @mleclercq! This PR introduces several important changes, so we need to make sure this is the right way to go :) A couple of things that I’m not super happy with:
The operations you have introduced to manipulate requests and responses remember me of the discussion in #47 (so, it also solves a problem we had for some time). Another thing middlewares should help us achieve is to implement #104. We should try to implement a “telemetry” middleware, as another validation. |
@mleclercq I have implemented my suggestion here, let me know what you think: mleclercq/endpoints@middleware-take-two...julienrf:just-authenticated-endpoints I have left some comments in the commit: julienrf@a3aac49 |
Hi, I believe this is going in an interesting direction (you can see how I could re-implement the basic authentication as a middleware). I still need to implement it for all the interpreters (so far I only did OpenAPI and play-client). Please let me know what you think about it. Maybe it’s introducing unnecessary complexity? |
Great to see some progress here! I don't think the Middleware approach makes it much more complicated. And it's nice that we can move some things to the algebras that used to live in the interpreters. I had to look twice to understand how we can have a client/server implementation in the algebra (since I saw the usage before the definition) but the ScalaDoc is pretty clear. Also I think it is important to note that using Middlewares is not required. And we currently have quite complicated code because of a lack of 'middlewares/composability'. A minor point. I think it would be nice if |
Hello! I managed to get everything implemented. You can see the result here: master...julienrf:just-authenticated-endpoints There are still a few things that are missing (tests, examples, and documentation). Also, some things to polish. More importantly, middlewares are currently made of “synchronous” functions (ie, they can’t return a Since the diff is quite large, here are the main highlights:
In conclusion, I still want to conduct a few more experiences but so far I am convinced that the changes proposed by @mleclercq should be accepted. As a first step, we could have a PR that only introduces the operations for transforming requests and responses (without the |
Great work @julienrf! Can you explain to me why we need Other than that I think two PRs would be good. The In any case I am willing to help and have some time next week. |
I think it will be easier to introduce things gradually, so I am in favor of doing separate PRs. Unfortunately, I won’t have much time in the next two months, so any help is appreciated. Maybe one of you @bmeesters or @mleclercq could create a PR introducing |
Ah yes, that explanation for If you want a shot @mleclercq at the transformations just let me know. Otherwise I will start on it next week. |
Addresses #794 and part of #844
The
Middlewares
algebra defines methods to change theRequest[A]
andResponse[B]
in anEndpoint[A, B]
.It also adds methods to add headers to
Request
andResponse
and add queryString toRequest
.While the
Middlewares
algebra is separate from theEndpoints
algebra, allEndpoints
interpreters implements it. To do so, several interpreters had to be changed, especially the client ones in order to be able to retrieve theRequest
andResponse
from anEndpoint
. For instance in akka-http-client the type ofEndpoint
is changed fromA => Future[B]
to:The
AuthenticatedEndpoints
algebra demonstrates a usage of these new capabilities. The client and server interpreters for this algebra are independent from the underlying HTTP framework and only uses the new operations provided byMiddlewares
. SinceAuthenticatedEndpoints
provides similar features as theBasicAuthentication
algebra, it has been deprecated.