{-# LANGUAGE OverloadedStrings #-}
module Smith.Client.Config (
Smith (..)
, SmithEndpoint (..)
, SmithCredentialsType (..)
, SmithCredentials (..)
, SmithScope (..)
, configure
, configureWith
, configureT
, configureWithT
, configureEndpoint
, configureOAuth2
, configureCredentials
, configureCredentialsByteString
, configureCredentialsFile
, SmithConfigureError (..)
, renderSmithConfigureError
) where
import Control.Monad.IO.Class (MonadIO (..))
import Control.Monad.Trans.Except (ExceptT (..), runExceptT)
import Crypto.JWT (JWK)
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Types as Aeson
import Data.Aeson ((.:))
import Data.ByteString (ByteString)
import qualified Data.ByteString as ByteString
import Data.Int (Int64)
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import qualified Network.HTTP.Client as HTTP
import qualified Network.HTTP.Client.TLS as HTTP
import qualified Network.OAuth2.JWT.Client as OAuth2
import Smith.Client.Data.Identity (IdentityId (..))
import qualified System.Directory as Directory
import qualified System.Environment as Environment
import System.FilePath (FilePath, (</>))
data Smith =
Smith SmithEndpoint HTTP.Manager OAuth2.Store
newtype SmithEndpoint =
SmithEndpoint {
getSmithEndpoint :: Text
} deriving (Eq, Ord, Show)
data SmithCredentialsType =
EnvironmentCredentials
| SmithHomeCredentials FilePath
| HomeCredentials FilePath
| SuppliedCredentials
deriving (Eq, Ord, Show)
data SmithCredentials =
SmithCredentials SmithCredentialsType IdentityId JWK
deriving (Eq, Show)
data SmithScope =
ProfileScope
| CAScope
deriving (Eq, Ord, Show, Enum, Bounded)
toOAuth2 :: SmithScope -> OAuth2.Scope
toOAuth2 s =
case s of
ProfileScope ->
OAuth2.Scope "profile"
CAScope ->
OAuth2.Scope "ca"
configure :: IO (Either SmithConfigureError Smith)
configure =
runExceptT configureT
configureT :: ExceptT SmithConfigureError IO Smith
configureT = do
liftIO (HTTP.newManager HTTP.tlsManagerSettings) >>=
configureWithT [minBound .. maxBound]
configureWith :: [SmithScope] -> HTTP.Manager -> IO (Either SmithConfigureError Smith)
configureWith scopes manager =
runExceptT $ configureWithT scopes manager
configureWithT :: [SmithScope] -> HTTP.Manager -> ExceptT SmithConfigureError IO Smith
configureWithT scopes manager = do
endpoint <- liftIO $ configureEndpoint
SmithCredentials _ issuer jwk <- configureCredentials
oauth2 <- liftIO $ configureOAuth2 manager endpoint (toOAuth2 <$> scopes) jwk issuer
pure $ Smith endpoint manager oauth2
configureEndpoint :: IO SmithEndpoint
configureEndpoint =
maybe (SmithEndpoint "https://api.smith.st") (SmithEndpoint . Text.pack) <$>
Environment.lookupEnv "SMITH_ENDPOINT"
configureOAuth2 :: HTTP.Manager -> SmithEndpoint -> [OAuth2.Scope] -> JWK -> IdentityId -> IO OAuth2.Store
configureOAuth2 manager endpoint scopes jwk issuer =
let
token =
OAuth2.TokenEndpoint $
mconcat [getSmithEndpoint endpoint, "/oauth/token"]
claims =
OAuth2.Claims
(OAuth2.Issuer $ identityId issuer)
Nothing
(OAuth2.Audience "https://smith.st")
scopes
(OAuth2.ExpiresIn 3600)
[]
in
OAuth2.newStore manager token claims jwk
configureCredentials :: ExceptT SmithConfigureError IO SmithCredentials
configureCredentials = do
j <- liftIO $ Environment.lookupEnv "SMITH_JWK"
case j of
Nothing -> do
s <- liftIO $ Environment.lookupEnv "SMITH_HOME"
case s of
Nothing -> do
h <- liftIO Directory.getHomeDirectory
let path = h </> ".smith" </> "credentials.json"
configureCredentialsFile (SmithHomeCredentials path) path
Just ss -> do
let path = ss </> "credentials.json"
configureCredentialsFile (SmithHomeCredentials path) path
Just s ->
configureCredentialsByteString EnvironmentCredentials . Text.encodeUtf8 . Text.pack $ s
configureCredentialsByteString :: SmithCredentialsType -> ByteString -> ExceptT SmithConfigureError IO SmithCredentials
configureCredentialsByteString t keydata = do
json <- case Aeson.decodeStrict keydata of
Nothing ->
left $ SmithConfigureJsonDecodeError t
Just v ->
pure v
jwk <- case Aeson.decodeStrict keydata of
Nothing ->
left $ SmithConfigureJwkDecodeError t
Just v ->
pure v
issuer <- case Aeson.parse (Aeson.withObject "JWK" $ \o -> o .: "smith.st/identity-id") json of
Aeson.Error _msg ->
left $ SmithConfigureIdentityIdDecodeError t
Aeson.Success v ->
pure $ IdentityId . Text.pack . show $ (v :: Int64)
pure $ SmithCredentials t issuer jwk
configureCredentialsFile :: SmithCredentialsType -> FilePath -> ExceptT SmithConfigureError IO SmithCredentials
configureCredentialsFile t path = do
exists <- liftIO . Directory.doesFileExist $ path
case exists of
False ->
left $ SmithConfigureCredentialsNotFound t
True -> do
bytes <- liftIO . ByteString.readFile $ path
configureCredentialsByteString t bytes
data SmithConfigureError =
SmithConfigureCredentialsNotFound SmithCredentialsType
| SmithConfigureJsonDecodeError SmithCredentialsType
| SmithConfigureJwkDecodeError SmithCredentialsType
| SmithConfigureIdentityIdDecodeError SmithCredentialsType
deriving (Eq, Ord, Show)
renderSmithCredentialsType :: SmithCredentialsType -> Text
renderSmithCredentialsType t =
case t of
EnvironmentCredentials ->
"Credentials were found in environment using $SMITH_JWK."
SmithHomeCredentials path ->
mconcat ["Credentials were found using $SMITH_HOME: ", Text.pack path]
HomeCredentials path ->
mconcat ["Credentials were found using $HOME: ", Text.pack path]
SuppliedCredentials ->
"Credentials were supplied programatically."
renderSmithConfigureError :: SmithConfigureError -> Text
renderSmithConfigureError e =
case e of
SmithConfigureCredentialsNotFound t ->
mconcat ["Smith credentials not found. ", case t of
SuppliedCredentials ->
"It looks you are using the library programatically, consider using 'configure'."
SmithHomeCredentials path ->
mconcat ["$SMITH_HOME is set, using $SMITH_HOME/credentials.json, but credentials file was not found: ", Text.pack path]
HomeCredentials path ->
mconcat ["$SMITH_HOME is not set, defaulting to $HOME/.smith/credentials.json, but credentials file was not found: ", Text.pack path]
EnvironmentCredentials ->
"Attempted to use $SMITH_JWK, but it was not set."]
SmithConfigureJsonDecodeError t ->
mconcat ["Smith credentials are not valid json and could not be decoded. ", renderSmithCredentialsType t]
SmithConfigureJwkDecodeError t ->
mconcat ["Smith credentials do not contain a valid JWT and could not be decoded. ", renderSmithCredentialsType t]
SmithConfigureIdentityIdDecodeError t ->
mconcat ["Smith credentials do not contain a valid identity and could not be decoded. ", renderSmithCredentialsType t]
left :: Applicative m => x -> ExceptT x m a
left =
ExceptT . pure . Left