-
Notifications
You must be signed in to change notification settings - Fork 64
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鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] AuthedContext implementation #184
Conversation
One more point, the auth part is not visible to swagger (what headers or auth info is required). |
Have you seen the function def genericRequestHeaderCapture[R](f: Request => ResultResponse[R]): TypedHeader[R :: HNil] and the object UserAuth {
case class User(name: String, id: UUID)
implicit val userAuthInfo = new AuthInfo[User] {
...
}
}
object Auth {
def authRule[T: AuthInfo]: TypedHeader[T :: HNil] =
genericRequestHeaderCapture { request =>
auth stuff...
}
}
val myService = new RhoService {
GET / "auth" >>> Auth.authRule[UserAuth.User] |>> { user: UserAuth.User =>
whatever...
}
} |
Ok, yeah that would be a lot less everything 馃憛 |
Sounds like I didn't fully understand what you're trying to do. I'll take another look at it this evening. |
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 don't know what exactly is going on, but it seems to me that the main goal is to turn a rho generated HttpService
into a AuthedService
, eg Kleisli[Task, Request, Response] => Kleisli[Task, (U, Request), Response]
(basically an inverse AuthMiddleware[U]
) by stuffing the U
into an attribute entry and pulling it back out in the service definition. The first part can be done via composition and I think the nicest way to do the second part would be via a path rule made with genericRequestHeaderCapture
, but a simple extractor is fine too.
* @param ev [[AuthInfo]] instance for U | ||
* @return An `AuthedService` | ||
*/ | ||
def foldAuthedServices[U](routes: Seq[RhoRoute.Tpe], filter: RhoMiddleware = identity)(implicit ev: AuthInfo[U]): AuthedService[U] = { |
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 can't this live in AuthedServiceBuilder
? Furthermore, it seems like it's only purpose is to add the entry to the attribute map so couldn't it be
val rhoService = foldServices(routes, filter)
Service.lift { case AuthedRequest(auth, req) =>
rhoService(req.withAttribute ...)
}
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 just wanted to conform to the overall existing structure.
* | ||
* @tparam U Authentication info type for ex: User(id: UUID...) | ||
*/ | ||
trait AuthInfo[U] { |
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 serialize and then deserialize the type U
when stuffing U
in and pulling it out of the attribute map? Why not just put type U
in there?
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.
Didn't realise attributeMap allows to add any T !
new AuthedRhoService(serviceBuilder.routes().map { prefix /: _ })(ev) | ||
} | ||
|
||
def withAuth[R](req: Request)(f: U => R): R = { |
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 a closure here? It doesn't do anything but extract U
, so why not make it a simple getter:
def getUath[U](req: Request): U = ...
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.
No reason, just an artefact of my usages in the project 馃憛
@bryce-anderson thanks for taking the time to review. I have addressed the review comments, any stupidity is purely unintentional 馃槃 |
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 we're getting closer.
import scala.collection.immutable.VectorBuilder | ||
|
||
/** CompileService which accumulates routes and can build a `AuthedService` */ | ||
final class AuthedServiceBuilder[U] private(internalRoutes: VectorBuilder[RhoRoute.Tpe]) extends ServiceBuilder(internalRoutes) { |
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 don't think we need this class at all: the important part
def toAuthedService(filter: RhoMiddleware = identity): AuthedService[U] = {
val rhoService = toService(filter)
Service.lift { case AuthedRequest(authInfo, req) =>
rhoService(req.withAttribute[U](authKey, authInfo))
}
}
can be inlined into AuthedRhoService
.
* @param routes Routes to prepend before elements in the constructor. | ||
* | ||
*/ | ||
class AuthedRhoService[U](routes: Seq[RhoRoute[_ <: HList]] = Vector.empty) |
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 convinced this needs to be a full DSL: all you really want is the toAuthedService
and getAuth[U]
functions which you could just use from an instance of this without having all the noise of another DSL class. This also helps to mitigate the AttributeKey
issue described below.
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 am not sure if I understand what you mean.
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.
What I mean is that function composition gives you the vast majority of stuff you need. For example, the class below would work for you:
class AuthContext[T] {
private val key = AttributeKey[U]("key")
def getUser(req: Request): T = req.attributes.get(key).get
def transform(rhoService: Service[Request, Response]): Service[AuthedRequest[U], Response] =
Service.lift { case AuthedRequest(authInfo, req) =>
rhoService(req.withAttribute[U](key, authInfo))
}
}
Then you just need to have this instance's getUser
method when you construct your RhoService
so you can actually get your user, and then use the same instance to transform it into an AuthedService
. Note that this isn't a rho specific tool. The only downside is that nobody is stopping you from forgetting the second part and then getUser
will explode since it clearly won't find the user in the attribute map. That last part is the only thing that AuthedRhoService
really buys us.
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 downside enough incentive to have a minimal DSL on AuthedRhoService?
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.
In fact I think, it would be very unpleasant (maybe even impossible) to do it without making it a DSL. You can't call getAuth inside a RhoService which you need to give to transform.
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.
It's totally possible and not that bad:
object MyAuth extends AuthContext[User]
object MyService extends RhoService {
import MyAuth._
GET/ "hello" |>> (req: Request) => {
val user = getAuth(req)
???
}
}
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 are right !! Making changes.
* @return A new [[AuthedRhoService]] that contains the routes of the other service appended | ||
* the the routes contained in this service. | ||
*/ | ||
final def and(other: AuthedRhoService[U]): AuthedRhoService[U] = new AuthedRhoService(this.getRoutes ++ other.getRoutes) |
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.
This is going to have problems with the referential equality of AttributeKey
: when you define a route, you use the getAuth
of the DSL instance where it is defined. However, the conversion of the routes to an AuthedService
uses the key from the DSL instance performing the compilation, resulting in different keys.
@@ -7,7 +7,7 @@ import shapeless.HList | |||
import scala.collection.immutable.VectorBuilder | |||
|
|||
/** CompileService which accumulates routes and can build a `HttpService` */ | |||
final class ServiceBuilder private(internalRoutes: VectorBuilder[RhoRoute.Tpe]) extends CompileService[RhoRoute.Tpe] { | |||
class ServiceBuilder (internalRoutes: VectorBuilder[RhoRoute.Tpe]) extends CompileService[RhoRoute.Tpe] { |
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.
See the comments above about why we don't need to extend this. If you're going to change this file at all lets make the class private as well.
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 like where this landed. My only points of discussion are names: AuthedRhoService
isn't really a service. How about AuthedContext
? Along those lines, does this belong in rho? Nothing in here is rho specific other than the names and that it turns a RhoService
into an AuthedService
, where it could just as well receive a generic HttpService
. I'm not against it, but it could just as well live in http4s, albeit that projects style tends to frown on things like Option.get
etc.
The only reason for using RhoService instead of HttpService was just to remove one more toService call on user side. Calling get here removes some boilerplate on the user side. In my opinion it's not too bad since a wrong implementation (where service is not an AuthedService) would fail pretty soon in a project and is a done deal (you do it only once). I don't completely agree that this is not rho specific because it's all about making rho work with the auth middleware of http4s. That said, this solves my purpose and it's ok if it's not merged. I did see two issues about this so maybe this is needed elsewhere too. But it's entirely your call :) |
I'm happy to merge it into rho and if others find utility outside, we can talk about moving it to http4s proper. I can merge it as is, or if you're interested, we could integrate this with the rho dsl using a custom path rule: final val auth: TypedHeader[U :: HNil] = rho.genericRequestHeaderCapture { req =>
req.attributes.get(authKey) match {
case Some(user) => SuccessResponse(user)
case None => FailureResponse.error("Invalid auth configuration")
}
} Then we can use it in the dsl like such: object MyService extends RhoService {
import MyAuth._
GET +? param("foo", "bar") >>> auth |>> { (req: Request, foo: String, user: User) =>
if (user.name == "Test User") {
Ok(s"just root with parameter 'foo=$foo'")
} else {
BadRequest("This should not have happened.")
}
}
} With even a little more work, that custom rule could probably include the metadata necessary for integration with the generated swagger docs, but that might be a significant amount of more work 馃槃. If you're not interested in the dsl integration I can take care of it later. Thanks for the good work, and sorry this has taken so long but I think we've arrived at a really good commit. |
Thanks @bryce-anderson 馃槃 |
I copy pasted your DSL code. I have no idea how to capture what http4s auth middleware is doing though. |
Merged! Thanks for the PR, and once again sorry for the lengthy back and forth but I think it paid off. |
Thanks guys. Will this be usable in a snapshot version perhaps? Authentication is the only thing that prevents us from using Rho at the moment. |
@bryce-anderson thanks for your time and patience 馃槃 A release with this would be really helpful. Please let me know if you would like me to work on the swagger thing. |
Let me check when the next milestone release is for http4s proper: if it's close I'd like to wait for it. |
An uninformed and naive attempt at implementing an AuthedRhoService for #14
The code is very likely poor quality and needs many changes, I hope it's not too bad 馃槢
The test is not working at the moment. Will need help with that.
Tested separately with a dummy API, seems to work with http4s authentication middleware.