keycloak-hs-1.1.0: src/Keycloak/Client.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns #-}
module Keycloak.Client where
import Control.Lens hiding ((.=))
import Control.Monad.Reader as R
import qualified Control.Monad.Catch as C
import Control.Monad.Except (throwError, catchError, MonadError)
import Data.Aeson as JSON
import Data.Aeson.Types hiding ((.=))
import Data.Text as T hiding (head, tail, map, lookup)
import Data.Text.Encoding
import Data.Maybe
import Data.Either
import Data.List as L
import Data.Map hiding (map, lookup)
import Data.String.Conversions
import Data.Monoid hiding (First)
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as BL
import Keycloak.Types
import Network.HTTP.Client as HC hiding (responseBody)
import Network.HTTP.Types.Status
import Network.HTTP.Types.Method
import Network.HTTP.Types (renderQuery)
import Network.Wreq as W hiding (statusCode)
import Network.Wreq.Types
import System.Log.Logger
import System.IO.Unsafe
import Web.JWT as JWT
import Safe
-- * Permissions
-- | Returns true if the resource is authorized under the given scope.
isAuthorized :: ResourceId -> ScopeName -> Token -> Keycloak Bool
isAuthorized res scope tok = do
r <- try $ checkPermission res scope tok
case r of
Right _ -> return True
Left e | (statusCode <$> getErrorStatus e) == Just 403 -> return False
Left e -> throwError e --rethrow the error
-- | Return the permissions for the permission requests.
getPermissions :: [PermReq] -> Token -> Keycloak [Permission]
getPermissions reqs tok = do
debug "Get all permissions"
client <- asks _clientId
let dat = ["grant_type" := ("urn:ietf:params:oauth:grant-type:uma-ticket" :: Text),
"audience" := client,
"response_mode" := ("permissions" :: Text)]
<> map (\p -> "permission" := p) (join $ map getPermString reqs)
body <- keycloakPost "protocol/openid-connect/token" dat tok
case eitherDecode body of
Right ret -> do
debug $ "Keycloak returned perms: " ++ (show ret)
return ret
Left (err2 :: String) -> do
debug $ "Keycloak parse error: " ++ (show err2)
throwError $ ParseError $ pack (show err2)
where
getPermString :: PermReq -> [Text]
getPermString (PermReq (Just (ResourceId id)) []) = [id]
getPermString (PermReq (Just (ResourceId id)) scopes) = map (\(ScopeName s) -> (id <> "#" <> s)) scopes
getPermString (PermReq Nothing scopes) = map (\(ScopeName s) -> ("#" <> s)) scopes
-- | Checks if a scope is permitted on a resource. An HTTP Exception 403 will be thrown if not.
checkPermission :: ResourceId -> ScopeName -> Token -> Keycloak ()
checkPermission (ResourceId res) (ScopeName scope) tok = do
debug $ "Checking permissions: " ++ (show res) ++ " " ++ (show scope)
client <- asks _clientId
let dat = ["grant_type" := ("urn:ietf:params:oauth:grant-type:uma-ticket" :: Text),
"audience" := client,
"permission" := res <> "#" <> scope]
keycloakPost "protocol/openid-connect/token" dat tok
return ()
-- * Tokens
-- | Retrieve the user's token. This token will be used for every other Keycloak calls.
getUserAuthToken :: Username -> Password -> Keycloak Token
getUserAuthToken username password = do
debug "Get user token"
client <- asks _clientId
secret <- asks _clientSecret
let dat = ["client_id" := client,
"client_secret" := secret,
"grant_type" := ("password" :: Text),
"password" := password,
"username" := username]
body <- keycloakPost' "protocol/openid-connect/token" dat
debug $ "Keycloak: " ++ (show body)
case eitherDecode body of
Right ret -> do
debug $ "Keycloak success: " ++ (show ret)
return $ Token $ convertString $ accessToken ret
Left err2 -> do
debug $ "Keycloak parse error: " ++ (show err2)
throwError $ ParseError $ pack (show err2)
-- | return a Client token. It is useful to create Resources.
getClientAuthToken :: Keycloak Token
getClientAuthToken = do
debug "Get client token"
client <- asks _clientId
secret <- asks _clientSecret
let dat = ["client_id" := client,
"client_secret" := secret,
"grant_type" := ("client_credentials" :: Text)]
body <- keycloakPost' "protocol/openid-connect/token" dat
case eitherDecode body of
Right ret -> do
debug $ "Keycloak success: " ++ (show ret)
return $ Token $ convertString $ accessToken ret
Left err2 -> do
debug $ "Keycloak parse error: " ++ (show err2)
throwError $ ParseError $ pack (show err2)
-- | Extract user name from a token
getUsername :: Token -> Username
getUsername (Token tok) = do
case JWT.decode $ convertString tok of
Just t -> case (unClaimsMap $ unregisteredClaims $ claims t) !? "preferred_username" of
Just (String un) -> un
_ -> error "preferred_username not present in token"
Nothing -> error "Error while decoding token"
-- * Resource
-- | Create an authorization resource in Keycloak, under the configured client.
createResource :: Resource -> Token -> Keycloak ResourceId
createResource r tok = do
debug $ convertString $ "Creating resource: " <> (JSON.encode r)
body <- keycloakPost "authz/protection/resource_set" (toJSON r) tok
debug $ convertString $ "Created resource: " ++ convertString body
case eitherDecode body of
Right ret -> do
debug $ "Keycloak success: " ++ (show ret)
return $ fromJustNote "create" $ resId ret
Left err2 -> do
debug $ "Keycloak parse error: " ++ (show err2)
throwError $ ParseError $ pack (show err2)
-- | Delete the resource
deleteResource :: ResourceId -> Token -> Keycloak ()
deleteResource (ResourceId rid) tok = do
--tok2 <- getClientAuthToken
keycloakDelete ("authz/protection/resource_set/" <> rid) tok
return ()
-- | Delete all resources in Keycloak
deleteAllResources :: Token -> Keycloak ()
deleteAllResources tok = do
debug "Deleting all Keycloak resources..."
ids <- getAllResourceIds
res <- mapM (\rid -> try $ deleteResource rid tok) ids
debug $ "Deleted " ++ (show $ L.length $ rights res) ++ " resources out of " ++ (show $ L.length ids)
-- | get a single resource
getResource :: ResourceId -> Token -> Keycloak Resource
getResource (ResourceId rid) tok = do
body <- keycloakGet ("authz/protection/resource_set/" <> rid) tok
case eitherDecode body of
Right ret -> do
return ret
Left (err2 :: String) -> do
debug $ "Keycloak parse error: " ++ (show err2)
throwError $ ParseError $ pack (show err2)
-- | get all resources IDs
getAllResourceIds :: Keycloak [ResourceId]
getAllResourceIds = do
debug "Get all resources"
tok2 <- getClientAuthToken
body <- keycloakGet ("authz/protection/resource_set?max=1000") tok2
case eitherDecode body of
Right ret -> do
return ret
Left (err2 :: String) -> do
debug $ "Keycloak parse error: " ++ (show err2)
throwError $ ParseError $ pack (show err2)
-- | Update a resource
updateResource :: Resource -> Token -> Keycloak ResourceId
updateResource = createResource
-- * Users
-- | Get users. Default number of users is 100. Parameters max and first allow to paginate and retrieve more than 100 users.
getUsers :: Maybe Max -> Maybe First -> Maybe Username -> Token -> Keycloak [User]
getUsers max first username tok = do
let query = maybe [] (\m -> [("max", Just $ convertString $ show m)]) max
++ maybe [] (\f -> [("first", Just $ convertString $ show f)]) first
++ maybe [] (\u -> [("username", Just $ convertString u)]) username
body <- keycloakAdminGet ("users" <> (convertString $ renderQuery True query)) tok
debug $ "Keycloak success"
case eitherDecode body of
Right ret -> do
debug $ "Keycloak success: " ++ (show ret)
return ret
Left (err2 :: String) -> do
debug $ "Keycloak parse error: " ++ (show err2)
throwError $ ParseError $ pack (show err2)
-- | Get a single user, based on his Id
getUser :: UserId -> Token -> Keycloak User
getUser (UserId id) tok = do
body <- keycloakAdminGet ("users/" <> (convertString id)) tok
debug $ "Keycloak success: " ++ (show body)
case eitherDecode body of
Right ret -> do
debug $ "Keycloak success: " ++ (show ret)
return ret
Left (err2 :: String) -> do
debug $ "Keycloak parse error: " ++ (show err2)
throwError $ ParseError $ pack (show err2)
-- | Create a user
createUser :: User -> Token -> Keycloak UserId
createUser user tok = do
res <- keycloakAdminPost ("users/") (toJSON user) tok
debug $ "Keycloak success: " ++ (show res)
return $ UserId $ convertString res
-- | Get a single user, based on his Id
updateUser :: UserId -> User -> Token -> Keycloak ()
updateUser (UserId id) user tok = do
keycloakAdminPut ("users/" <> (convertString id)) (toJSON user) tok
return ()
-- * Keycloak basic requests
-- | Perform post to Keycloak.
keycloakPost :: (Postable dat, Show dat) => Path -> dat -> Token -> Keycloak BL.ByteString
keycloakPost path dat tok = do
(KCConfig baseUrl realm _ _) <- ask
let opts = W.defaults & W.header "Authorization" .~ ["Bearer " <> (unToken tok)]
let url = (unpack $ baseUrl <> "/realms/" <> realm <> "/" <> path)
info $ "Issuing KEYCLOAK POST with url: " ++ (show url)
debug $ " data: " ++ (show dat)
--debug $ " headers: " ++ (show $ opts ^. W.headers)
eRes <- C.try $ liftIO $ W.postWith opts url dat
case eRes of
Right res -> do
return $ fromJust $ res ^? responseBody
Left err -> do
warn $ "Keycloak HTTP error: " ++ (show err)
throwError $ HTTPError err
-- | Perform post to Keycloak, without token.
keycloakPost' :: (Postable dat, Show dat) => Path -> dat -> Keycloak BL.ByteString
keycloakPost' path dat = do
(KCConfig baseUrl realm _ _) <- ask
let opts = W.defaults
let url = (unpack $ baseUrl <> "/realms/" <> realm <> "/" <> path)
info $ "Issuing KEYCLOAK POST with url: " ++ (show url)
debug $ " data: " ++ (show dat)
--debug $ " headers: " ++ (show $ opts ^. W.headers)
eRes <- C.try $ liftIO $ W.postWith opts url dat
case eRes of
Right res -> do
return $ fromJust $ res ^? responseBody
Left err -> do
warn $ "Keycloak HTTP error: " ++ (show err)
throwError $ HTTPError err
-- | Perform delete to Keycloak.
keycloakDelete :: Path -> Token -> Keycloak ()
keycloakDelete path tok = do
(KCConfig baseUrl realm _ _) <- ask
let opts = W.defaults & W.header "Authorization" .~ ["Bearer " <> (unToken tok)]
let url = (unpack $ baseUrl <> "/realms/" <> realm <> "/" <> path)
info $ "Issuing KEYCLOAK DELETE with url: " ++ (show url)
debug $ " headers: " ++ (show $ opts ^. W.headers)
eRes <- C.try $ liftIO $ W.deleteWith opts url
case eRes of
Right res -> return ()
Left err -> do
warn $ "Keycloak HTTP error: " ++ (show err)
throwError $ HTTPError err
-- | Perform get to Keycloak on admin API
keycloakGet :: Path -> Token -> Keycloak BL.ByteString
keycloakGet path tok = do
(KCConfig baseUrl realm _ _) <- ask
let opts = W.defaults & W.header "Authorization" .~ ["Bearer " <> (unToken tok)]
let url = (unpack $ baseUrl <> "/realms/" <> realm <> "/" <> path)
info $ "Issuing KEYCLOAK GET with url: " ++ (show url)
debug $ " headers: " ++ (show $ opts ^. W.headers)
eRes <- C.try $ liftIO $ W.getWith opts url
case eRes of
Right res -> do
return $ fromJust $ res ^? responseBody
Left err -> do
warn $ "Keycloak HTTP error: " ++ (show err)
throwError $ HTTPError err
-- | Perform get to Keycloak on admin API
keycloakAdminGet :: Path -> Token -> Keycloak BL.ByteString
keycloakAdminGet path tok = do
(KCConfig baseUrl realm _ _) <- ask
let opts = W.defaults & W.header "Authorization" .~ ["Bearer " <> (unToken tok)]
let url = (unpack $ baseUrl <> "/admin/realms/" <> realm <> "/" <> path)
info $ "Issuing KEYCLOAK GET with url: " ++ (show url)
debug $ " headers: " ++ (show $ opts ^. W.headers)
eRes <- C.try $ liftIO $ W.getWith opts url
case eRes of
Right res -> do
return $ fromJust $ res ^? responseBody
Left err -> do
warn $ "Keycloak HTTP error: " ++ (show err)
throwError $ HTTPError err
-- | Perform post to Keycloak.
keycloakAdminPost :: (Postable dat, Show dat) => Path -> dat -> Token -> Keycloak BL.ByteString
keycloakAdminPost path dat tok = do
(KCConfig baseUrl realm _ _) <- ask
let opts = W.defaults & W.header "Authorization" .~ ["Bearer " <> (unToken tok)]
let url = (unpack $ baseUrl <> "/admin/realms/" <> realm <> "/" <> path)
info $ "Issuing KEYCLOAK POST with url: " ++ (show url)
debug $ " data: " ++ (show dat)
--debug $ " headers: " ++ (show $ opts ^. W.headers)
eRes <- C.try $ liftIO $ W.postWith opts url dat
case eRes of
Right res -> do
debug $ (show eRes)
let headers = fromJust $ res ^? W.responseHeaders
return $ convertString $ L.last $ T.split (== '/') $ convertString $ fromJust $ lookup "Location" headers
Left err -> do
warn $ "Keycloak HTTP error: " ++ (show err)
throwError $ HTTPError err
-- | Perform put to Keycloak.
keycloakAdminPut :: (Putable dat, Show dat) => Path -> dat -> Token -> Keycloak ()
keycloakAdminPut path dat tok = do
(KCConfig baseUrl realm _ _) <- ask
let opts = W.defaults & W.header "Authorization" .~ ["Bearer " <> (unToken tok)]
let url = (unpack $ baseUrl <> "/admin/realms/" <> realm <> "/" <> path)
info $ "Issuing KEYCLOAK PUT with url: " ++ (show url)
debug $ " data: " ++ (show dat)
debug $ " headers: " ++ (show $ opts ^. W.headers)
eRes <- C.try $ liftIO $ W.putWith opts url dat
case eRes of
Right res -> return ()
Left err -> do
warn $ "Keycloak HTTP error: " ++ (show err)
throwError $ HTTPError err
-- * Helpers
debug, warn, info, err :: (MonadIO m) => String -> m ()
debug s = liftIO $ debugM "Keycloak" s
info s = liftIO $ infoM "Keycloak" s
warn s = liftIO $ warningM "Keycloak" s
err s = liftIO $ errorM "Keycloak" s
getErrorStatus :: KCError -> Maybe Status
getErrorStatus (HTTPError (HttpExceptionRequest _ (StatusCodeException r _))) = Just $ HC.responseStatus r
getErrorStatus _ = Nothing
try :: MonadError a m => m b -> m (Either a b)
try act = catchError (Right <$> act) (return . Left)