cdepillabout / servant-rawm

Effectful Raw handler for Servant servers.
https://hackage.haskell.org/package/servant-rawm
BSD 3-Clause "New" or "Revised" License
16 stars 7 forks source link

Question: please advise on how to combine RawM with servant-auth's Auth combinator #7

Open adetokunbo opened 5 years ago

adetokunbo commented 5 years ago

Hi, and thank you for producing a really useful extension to servant

I'm trying to use it with servant-auth. I declare a route that uses both RawM and Auth in a custom Monad e.g.

type ProtectedRaw = (Auth '[Cookie, JWT] Account) :> "protected" :> RawM

protectedRawR
  :: CookieSettings
  -> JWTSettings -> ServerT ProtectedRaw MainApp
protectedRawR = undefined

-- | The application-level Monad, provides acccess to the configuration
-- information via the Reader Monad.
type MainApp = ReaderT Env (ExceptT ServantErr IO)

-- | Convert a MainApp to a Handler
appToHandler :: Env -> MainApp a -> Handler a
appToHandler env action = do
  res <- liftIO $ runExceptT $ runReaderT action env
  liftEither res

testServer env = serveWithContext @ProtectedRaw Proxy theContext server'
  where
    xsrfs = def { xsrfExcludeGet = True }
    cs = defaultCookieSettings { cookieXsrfSetting = Just xsrfs }
    jwts =  defaultJWTSettings $ envSigningKey env
    theContext = cs :. jwts :. EmptyContext
    server' =
      hoistServerWithContext
      @ProtectedRaw Proxy
      (Proxy :: Proxy '[CookieSettings, JWTSettings])
      (appToHandler env) $ protectedRawR cs jwts

this fails to compile with the following message:

    • No instance for (HasServer
                         (Servant.Auth.Server.Internal.AddSetCookie.AddSetCookieApi
                            (Servant.Auth.Server.Internal.AddSetCookie.AddSetCookieApi
                               (Servant.RawM.Internal.API.RawM'
                                  Servant.RawM.Internal.API.FileServer)))
                         '[CookieSettings, JWTSettings])
        arising from a use of ‘hoistServerWithContext’
    • In the expression:
        hoistServerWithContext
          @ProtectedRaw
          Proxy
          (Proxy :: Proxy '[CookieSettings, JWTSettings])
          (appToHandler env)
      In the expression:
        hoistServerWithContext
          @ProtectedRaw
          Proxy
          (Proxy :: Proxy '[CookieSettings, JWTSettings])
          (appToHandler env)
          $ protectedRawR cs jwts
      In an equation for ‘server'’:
          server'
            = hoistServerWithContext
                @ProtectedRaw
                Proxy
                (Proxy :: Proxy '[CookieSettings, JWTSettings])
                (appToHandler env)
                $ protectedRawR cs jwts
    |
139 |       hoistServerWithContext
    |       ^^^^^^^^^^^^^^^^^^^^^^...

Is that expected ? Is there something wrong with how hoistServerWithContext is used here? Will it be necessary to write a distinct HasServer instance that combines the two combinators ?

cdepillabout commented 5 years ago

You can simplify the error message to something like the following:

No instance for (HasServer
                         (AddSetCookieApi (AddSetCookieApi RawM))
                         '[CookieSettings, JWTSettings])

If you look at AddSetCookieApi, you can see what is going on:

https://hackage.haskell.org/package/servant-auth-server-0.4.0.0/docs/Servant-Auth-Server-Internal-AddSetCookie.html#t:AddSetCookieApi

AddSetCookieApi is a type family. It has an instance for Raw (and some other things), but no instance for RawM.

I think this error is occurring because there is no instance for RawM.

I think there are a couple ways to fix this. The "best" way to fix it would be create a package called servant-auth-rawm that defines the AddSetCookieApi instance for RawM. When users want to use servant-auth and servant-rawm together, they would need to also use the package servant-auth-rawm.

Another solution would be to just add this instance to servant-rawm itself. servant-rawm would get a dependency on servant-auth, but I don't know if this is that bad of a thing. Are there that many users that want to use servant-server and servant-rawm, but absolutely don't want to have servant-auth as a dependency? I dunno, but I wouldn't think there are too many. If one of them are unhappy about this, they could always send a PR creating a servant-auth-rawm` package.

The hackiest (but easiest) solution would just be to create the type instance in your own code.

I think it should look like this:

type instance AddSetCookieApi (RawM' a) = RawM' a
adetokunbo commented 5 years ago

Thank you for the quick response!

I have just tried this out, and I get a different error, which I think may also be related to type family instances.

    • Couldn't match type ‘Network.Wai.Internal.Request
                           -> (Network.Wai.Internal.Response
                               -> IO Network.Wai.Internal.ResponseReceived)
                           -> IO Network.Wai.Internal.ResponseReceived’
                     with ‘Headers '[Header "Set-Cookie" SetCookie] cookied1’
        arising from a use of ‘hoistServerWithContext’
    • In the expression:
        hoistServerWithContext
          @ProtectedRaw
          Proxy
          (Proxy :: Proxy '[CookieSettings, JWTSettings])
          (appToHandler env)
      In the expression:
        hoistServerWithContext
          @ProtectedRaw
          Proxy
          (Proxy :: Proxy '[CookieSettings, JWTSettings])
          (appToHandler env)
          $ protectedRawR cs jwts
      In an equation for ‘server'’:
          server'
            = hoistServerWithContext
                @ProtectedRaw
                Proxy
                (Proxy :: Proxy '[CookieSettings, JWTSettings])
                (appToHandler env)
                $ protectedRawR cs jwts

This looks like it's just a type mismatch; it seems that the type computed for hoistServerWithContext is inconsistent between the HasServer instances of the RawM and Auth combinators.

Thanks for the pointer about the type families, there are additional ones used in the constraint pf the Auth HasServer instance, so I'm going to take a look at those to see if there additional type instances that need to be declared.

cdepillabout commented 5 years ago

Yeah, I'm not sure what this error is caused by!

If you have any trouble figuring it out, you can post a link to a repo and I'll dig in a little and try to figure it out. (If the code is private, then a minimal repro would work too.)

If you can figure out what the problem is, then packaging the fix up in a new repo or PR, or even just posting it here would probably be helpful for other people!

adetokunbo commented 5 years ago

Thanks! I also had to an AddSetCookies instance for Functor Application, which works because servant-auth already provides an instance for Application.

-- Type instance for RawM for SetCookie API
type instance AddSetCookieApi (RawM' a) = RawM' a

-- Provide an @AddSetCookies@ instance for functors of Application.
instance (Functor m)
  => AddSetCookies ('S n) (m Application) (m Application) where
  addSetCookies cookies = (fmap $ addSetCookies cookies)

I don't mind putting this into a package called servant-auth-rawm if you are happy to accept a PR.

cdepillabout commented 5 years ago

@adetokunbo Yeah, that works for me!