Implement different exceptions for different cases

I had hoped to get away from this entirely, to an Either-based
interface, but that seems to be stalling as an initiative. So in the
meantime, let's at least make our exceptions more meaningful.
This commit is contained in:
patrick brisbin 2018-09-18 16:56:57 -04:00
parent e3c61789ba
commit 1411bb5858
4 changed files with 52 additions and 41 deletions

View File

@ -5,15 +5,25 @@ module Yesod.Auth.OAuth2.Exception
) where ) where
import Control.Exception.Safe import Control.Exception.Safe
import qualified Data.ByteString.Lazy as BL import Data.ByteString.Lazy (ByteString)
import Data.Text (Text) import Data.Text (Text)
-- | Provider name and error data YesodOAuth2Exception
-- = OAuth2Error Text ByteString
-- The error is a lazy bytestring because it's most often encoded JSON. -- ^ HTTP error during OAuth2 handshake
-- --
-- Deprecated. Eventually, we'll return @Either@s all the way up. -- Plugin name and JSON-encoded @OAuth2Error@ from @hoauth2@.
-- --
data YesodOAuth2Exception = InvalidProfileResponse Text BL.ByteString | JSONDecodingError Text String
-- ^ User profile was not as expected
--
-- Plugin name and Aeson parse error message.
--
| GenericError Text String
-- ^ Other error conditions
--
-- Plugin name and error message.
--
deriving (Show, Typeable) deriving (Show, Typeable)
instance Exception YesodOAuth2Exception instance Exception YesodOAuth2Exception

View File

@ -10,6 +10,7 @@ import Control.Monad (unless)
import qualified Data.ByteString.Lazy.Char8 as BL8 import qualified Data.ByteString.Lazy.Char8 as BL8
import Network.HTTP.Client import Network.HTTP.Client
import qualified Network.HTTP.Types as HT import qualified Network.HTTP.Types as HT
import qualified Yesod.Auth.OAuth2.Exception as YesodOAuth2Exception
newtype User = User Text newtype User = User Text
@ -34,31 +35,34 @@ oauth2Nylas clientId clientSecret =
-- FIXME: was this working? I'm 95% sure that the client will throw its -- FIXME: was this working? I'm 95% sure that the client will throw its
-- own exception on unsuccessful status codes. -- own exception on unsuccessful status codes.
unless (HT.statusIsSuccessful $ responseStatus resp) unless (HT.statusIsSuccessful $ responseStatus resp)
$ throwIO $ InvalidProfileResponse pluginName $ throwIO
$ "Unsuccessful HTTP response: " <> userResponse $ YesodOAuth2Exception.GenericError pluginName
$ "Unsuccessful HTTP response: "
<> BL8.unpack userResponse
either either
(throwIO . InvalidProfileResponse pluginName . BL8.pack) (throwIO . YesodOAuth2Exception.JSONDecodingError pluginName)
(\(User userId) -> pure Creds (\(User userId) -> pure Creds
{ credsPlugin = pluginName { credsPlugin = pluginName
, credsIdent = userId , credsIdent = userId
, credsExtra = setExtra token userResponse , credsExtra = setExtra token userResponse
} }
) )
$ eitherDecode userResponse $ eitherDecode userResponse
where where
oauth = OAuth2 oauth = OAuth2
{ oauthClientId = clientId { oauthClientId = clientId
, oauthClientSecret = clientSecret , oauthClientSecret = clientSecret
, oauthOAuthorizeEndpoint = "https://api.nylas.com/oauth/authorize" `withQuery` , oauthOAuthorizeEndpoint = "https://api.nylas.com/oauth/authorize"
[ ("response_type", "code") `withQuery` [ ("response_type", "code")
, ("client_id", encodeUtf8 clientId) , ( "client_id"
, encodeUtf8 clientId
)
-- N.B. The scopes delimeter is unknown/untested. Verify that before -- N.B. The scopes delimeter is unknown/untested. Verify that before
-- extracting this to an argument and offering a Scoped function. In -- extracting this to an argument and offering a Scoped function. In
-- its current state, it doesn't matter because it's only one scope. -- its current state, it doesn't matter because it's only one scope.
, scopeParam "," defaultScopes , scopeParam "," defaultScopes
] ]
, oauthAccessTokenEndpoint = "https://api.nylas.com/oauth/token" , oauthAccessTokenEndpoint = "https://api.nylas.com/oauth/token"
, oauthCallback = Nothing , oauthCallback = Nothing
} }

View File

@ -52,7 +52,6 @@ module Yesod.Auth.OAuth2.Prelude
, module URI.ByteString.Extension , module URI.ByteString.Extension
-- * Temporary, until I finish re-structuring modules -- * Temporary, until I finish re-structuring modules
, YesodOAuth2Exception(..)
, authOAuth2 , authOAuth2
, authOAuth2Widget , authOAuth2Widget
) where ) where
@ -61,7 +60,6 @@ import Control.Exception.Safe
import Data.Aeson import Data.Aeson
import Data.ByteString (ByteString) import Data.ByteString (ByteString)
import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Lazy.Char8 as BL8
import Data.Semigroup ((<>)) import Data.Semigroup ((<>))
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
@ -72,7 +70,7 @@ import URI.ByteString
import URI.ByteString.Extension import URI.ByteString.Extension
import Yesod.Auth import Yesod.Auth
import Yesod.Auth.OAuth2 import Yesod.Auth.OAuth2
import Yesod.Auth.OAuth2.Exception import qualified Yesod.Auth.OAuth2.Exception as YesodOAuth2Exception
-- | Retrieve a user's profile as JSON -- | Retrieve a user's profile as JSON
-- --
@ -92,18 +90,17 @@ authGetProfile name manager token url = do
decoded <- fromAuthJSON name resp decoded <- fromAuthJSON name resp
pure (decoded, resp) pure (decoded, resp)
-- | Throws a @Left@ result as an @'InvalidProfileResponse'@ -- | Throws a @Left@ result as an @'YesodOAuth2Exception'@
fromAuthGet fromAuthGet
:: Text -> Either (OAuth2Error Value) BL.ByteString -> IO BL.ByteString :: Text -> Either (OAuth2Error Value) BL.ByteString -> IO BL.ByteString
fromAuthGet _ (Right bs) = pure bs -- nice fromAuthGet _ (Right bs) = pure bs -- nice
fromAuthGet name (Left err) = fromAuthGet name (Left err) =
throwIO $ InvalidProfileResponse name $ encode err throwIO $ YesodOAuth2Exception.OAuth2Error name $ encode err
-- | Throws a decoding error as an @'InvalidProfileResponse'@ -- | Throws a decoding error as an @'YesodOAuth2Exception'@
fromAuthJSON :: FromJSON a => Text -> BL.ByteString -> IO a fromAuthJSON :: FromJSON a => Text -> BL.ByteString -> IO a
fromAuthJSON name = fromAuthJSON name =
-- FIXME: unique exception constructors either (throwIO . YesodOAuth2Exception.JSONDecodingError name) pure
either (throwIO . InvalidProfileResponse name . BL8.pack) pure
. eitherDecode . eitherDecode
-- | A tuple of @\"scope\"@ and the given scopes separated by a delimiter -- | A tuple of @\"scope\"@ and the given scopes separated by a delimiter

View File

@ -15,6 +15,7 @@ import Yesod.Auth.OAuth2.Prelude
import Network.HTTP.Client import Network.HTTP.Client
(httpLbs, parseUrlThrow, responseBody, setQueryString) (httpLbs, parseUrlThrow, responseBody, setQueryString)
import Yesod.Auth.OAuth2.Exception as YesodOAuth2Exception
data SlackScope data SlackScope
= SlackBasicScope = SlackBasicScope
@ -53,21 +54,20 @@ oauth2SlackScoped scopes clientId clientSecret =
userResponse <- responseBody <$> httpLbs req manager userResponse <- responseBody <$> httpLbs req manager
either either
(const $ throwIO $ InvalidProfileResponse pluginName userResponse) (throwIO . YesodOAuth2Exception.JSONDecodingError pluginName)
(\(User userId) -> pure Creds (\(User userId) -> pure Creds
{ credsPlugin = pluginName { credsPlugin = pluginName
, credsIdent = userId , credsIdent = userId
, credsExtra = setExtra token userResponse , credsExtra = setExtra token userResponse
} }
) )
$ eitherDecode userResponse $ eitherDecode userResponse
where where
oauth2 = OAuth2 oauth2 = OAuth2
{ oauthClientId = clientId { oauthClientId = clientId
, oauthClientSecret = clientSecret , oauthClientSecret = clientSecret
, oauthOAuthorizeEndpoint = "https://slack.com/oauth/authorize" `withQuery` , oauthOAuthorizeEndpoint = "https://slack.com/oauth/authorize"
[ scopeParam "," $ map scopeText scopes `withQuery` [scopeParam "," $ map scopeText scopes]
]
, oauthAccessTokenEndpoint = "https://slack.com/api/oauth.access" , oauthAccessTokenEndpoint = "https://slack.com/api/oauth.access"
, oauthCallback = Nothing , oauthCallback = Nothing
} }