packages feed

keycloak-hs 0.0.0.0 → 0.0.0.1

raw patch · 4 files changed

+85/−85 lines, 4 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

+ Keycloak.Types: runKeycloak :: Keycloak a -> KCConfig -> IO (Either KCError a)
- Keycloak.Client: getUserAuthToken :: Text -> Text -> Keycloak Token
+ Keycloak.Client: getUserAuthToken :: Username -> Password -> Keycloak Token

Files

README.md view
@@ -1,7 +1,7 @@ keycloak-hs =========== -keycloak-hs is a library for connecting to Keycloak made in Haskell.+keycloak-hs is a library for connecting to [Keycloak](https://www.keycloak.org/) made in Haskell. Keycloak allows to authenticate users and protect resources.  Warning: This package is experimental and still under development.
keycloak-hs.cabal view
@@ -1,6 +1,6 @@ cabal-version: 1.12 name:           keycloak-hs-version:        0.0.0.0+version:        0.0.0.1 description:    Please see the README on GitHub at <https://github.com/cdupont/keycloak-hs#readme> homepage:       https://github.com/cdupont/keycloak-hs#readme bug-reports:    https://github.com/cdupont/keycloak-hs/issues
src/Keycloak/Client.hs view
@@ -32,11 +32,9 @@ import           System.IO.Unsafe  ----------------------- * Permissions ----------------------+-- * Permissions --- Checks is a scope is permitted on a resource. An HTTP Exception 403 will be thrown if not.+-- |Checks is a scope is permitted on a resource. An HTTP Exception 403 will be thrown if not. checkPermission :: ResourceId -> ScopeName -> Token -> Keycloak () checkPermission (ResourceId res) scope tok = do   debug $ "Checking permissions: " ++ (show res) ++ " " ++ (show scope)@@ -47,6 +45,7 @@   keycloakPost "protocol/openid-connect/token" dat tok   return () +-- | Returns true id 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@@ -55,6 +54,7 @@     Left e | (statusCode <$> getErrorStatus e) == Just 403 -> return False     Left e -> throwError e --rethrow the error +-- | Return the permissions for all resources, under the given scopes. getAllPermissions :: [ScopeName] -> Token -> Keycloak [Permission] getAllPermissions scopes tok = do   debug "Get all permissions"@@ -73,11 +73,10 @@       throwError $ ParseError $ pack (show err2)  ------------------ * Tokens ------------------  -getUserAuthToken :: Text -> Text -> Keycloak Token+-- * Tokens++-- | Retrieve the user's token+getUserAuthToken :: Username -> Password -> Keycloak Token getUserAuthToken username password = do    debug "Get user token"   client <- asks _clientId@@ -97,6 +96,7 @@       debug $ "Keycloak parse error: " ++ (show err2)        throwError $ ParseError $ pack (show err2) +-- | return a Client token getClientAuthToken :: Keycloak Token getClientAuthToken = do   debug "Get client token"@@ -121,6 +121,7 @@       Right td -> Right td       Left (e :: ParseError String) -> Left $ show e +-- | Extract user name from a token getUsername :: Token -> Maybe Username getUsername tok = do    case decodeToken tok of@@ -129,10 +130,10 @@       traceM $ "Error while decoding token: " ++ (show e)       Nothing -------------------- * Resource ------------------- +-- * Resource++-- | Create a resource. createResource :: Resource -> Token -> Keycloak ResourceId createResource r tok = do   debug $ convertString $ "Creating resource: " <> (JSON.encode r)@@ -146,16 +147,16 @@       debug $ "Keycloak parse error: " ++ (show err2)        throwError $ ParseError $ pack (show err2) +-- | Delete the resource deleteResource :: ResourceId -> Token -> Keycloak () deleteResource (ResourceId rid) tok = do   keycloakDelete ("authz/protection/resource_set/" <> rid) tok    return ()  ----------------- * Users ----------------+-- * 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 -> Token -> Keycloak [User] getUsers max first tok = do   let query = maybe [] (\l -> [("limit", Just $ convertString $ show l)]) max@@ -170,6 +171,7 @@       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 @@ -183,10 +185,9 @@       throwError $ ParseError $ pack (show err2)  ----------------------------- * Keycloak requests ------------------------------- Perform post to Keycloak.+-- * 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@@ -203,6 +204,7 @@       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@@ -219,7 +221,7 @@       warn $ "Keycloak HTTP error: " ++ (show err)       throwError $ HTTPError err --- Perform delete to Keycloak.+-- | Perform delete to Keycloak. keycloakDelete :: Path -> Token -> Keycloak () keycloakDelete path tok = do    (KCConfig baseUrl realm _ _) <- ask@@ -234,7 +236,7 @@       warn $ "Keycloak HTTP error: " ++ (show err)       throwError $ HTTPError err --- Perform get to Keycloak on admin API+-- | Perform get to Keycloak on admin API keycloakAdminGet :: Path -> Token -> Keycloak BL.ByteString keycloakAdminGet path tok = do    (KCConfig baseUrl realm _ _) <- ask@@ -251,9 +253,7 @@       throwError $ HTTPError err  ------------------- * Helpers ------------------+-- * Helpers  debug, warn, info, err :: (MonadIO m) => String -> m () debug s = liftIO $ debugM "API" s
src/Keycloak/Types.hs view
@@ -17,7 +17,7 @@ import qualified Data.ByteString as BS import qualified Data.Word8 as W8 (isSpace, _colon, toLower) import           Data.Char-import           Control.Monad.Except (ExceptT)+import           Control.Monad.Except (ExceptT, runExceptT) import           Control.Monad.Reader as R import           Control.Lens hiding ((.=)) import           GHC.Generics (Generic)@@ -25,44 +25,41 @@ import           Network.HTTP.Client as HC hiding (responseBody)  -------------------------- * Keycloak Monad -------------------------+-- * Keycloak Monad +-- | Keycloak Monad stack: a simple Reader monad containing the config, and an ExceptT to handle HTTPErrors and parse errors. type Keycloak a = ReaderT KCConfig (ExceptT KCError IO) a +-- | Contains HTTP errors and parse errors. data KCError = HTTPError HttpException  -- ^ Keycloak returned an HTTP error.              | ParseError Text          -- ^ Failed when parsing the response              | EmptyError               -- ^ Empty error to serve as a zero element for Monoid. +-- | Configuration of Keycloak. data KCConfig = KCConfig {   _baseUrl       :: Text,   _realm         :: Text,   _clientId      :: Text,   _clientSecret  :: Text} deriving (Eq, Show)---  _adminLogin    :: Username,---  _adminPassword :: Password,---  _guestLogin    :: Username,---  _guestPassword :: Password} +-- | Default configuration defaultKCConfig :: KCConfig defaultKCConfig = KCConfig {   _baseUrl       = "http://localhost:8080/auth",   _realm         = "waziup",   _clientId      = "api-server",   _clientSecret  = "4e9dcb80-efcd-484c-b3d7-1e95a0096ac0"}---  _adminLogin    = "cdupont",---  _adminPassword = "password",- -- _guestLogin    = "guest",- -- _guestPassword = "guest"} +-- | Run a Keycloak monad within IO.+runKeycloak :: Keycloak a -> KCConfig -> IO (Either KCError a)+runKeycloak kc conf = runExceptT $ runReaderT kc conf+ type Path = Text  ----------------- * Token ----------------+-- * Token +-- | Wrapper for tokens. newtype Token = Token {unToken :: BS.ByteString} deriving (Eq, Show, Generic)  instance FromJSON Token where@@ -84,29 +81,30 @@  instance ToHttpApiData Token where   toQueryParam (Token token) = "Bearer " <> (decodeUtf8 token)-  + +-- | Token description returned by Keycloak data TokenDec = TokenDec {-  jti :: Text,-  exp :: Int,-  nbf :: Int,-  iat :: Int,-  iss :: Text,-  aud :: Text,-  sub :: Text,-  typ :: Text,-  azp :: Text,-  authTime :: Int,-  sessionState :: Text,-  acr :: Text,-  allowedOrigins :: Value,-  realmAccess :: Value,-  ressourceAccess :: Value,-  scope :: Text,-  name :: Text,+  jti               :: Text,+  exp               :: Int,+  nbf               :: Int,+  iat               :: Int,+  iss               :: Text,+  aud               :: Text,+  sub               :: Text,+  typ               :: Text,+  azp               :: Text,+  authTime          :: Int,+  sessionState      :: Text,+  acr               :: Text,+  allowedOrigins    :: Value,+  realmAccess       :: Value,+  ressourceAccess   :: Value,+  scope             :: Text,+  name              :: Text,   preferredUsername :: Text,-  givenName :: Text,-  familyName :: Text,-  email :: Text+  givenName         :: Text,+  familyName        :: Text,+  email             :: Text   } deriving (Generic, Show)  parseTokenDec :: Parse e TokenDec@@ -133,12 +131,13 @@     AB.key "family_name" asText <*>     AB.key "email" asText ---------------------- * Permission --------------------- +-- * Permission++-- | Scope name type ScopeName = Text +-- | Scope Id newtype ScopeId = ScopeId {unScopeId :: Text} deriving (Show, Eq, Generic)  --JSON instances@@ -148,6 +147,7 @@ instance FromJSON ScopeId where   parseJSON = genericParseJSON (defaultOptions {unwrapUnaryRecords = True}) +-- | Keycloak scope data Scope = Scope {   scopeId   :: Maybe ScopeId,   scopeName :: ScopeName@@ -159,6 +159,7 @@ instance FromJSON Scope where   parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = unCapitalize . drop 5} +-- | Keycloak permission on a resource data Permission = Permission    { rsname :: ResourceName,     rsid   :: ResourceId,@@ -171,18 +172,15 @@ instance FromJSON Permission where   parseJSON = genericParseJSON defaultOptions -type Username = Text-type Password = Text ----------------- * User ---------------+-- * User +type Username = Text+type Password = Text type First = Int type Max = Int --- Id of a user+-- | Id of a user newtype UserId = UserId {unUserId :: Text} deriving (Show, Eq, Generic)  --JSON instances@@ -211,10 +209,11 @@ instance ToJSON User where   toJSON = genericToJSON defaultOptions {fieldLabelModifier = drop 4, omitNothingFields = True} ----------------- * Owner ---------------- ++-- * Owner++-- | A resource owner data Owner = Owner {   ownId   :: Maybe Text,   ownName :: Username@@ -227,12 +226,11 @@   toJSON = genericToJSON $ (aesonDrop 3 snakeCase) {omitNothingFields = True}  -------------------- * Resource -------------------+-- * Resource  type ResourceName = Text +-- | A resource Id newtype ResourceId = ResourceId {unResId :: Text} deriving (Show, Eq, Generic)  -- JSON instances@@ -242,15 +240,16 @@ instance FromJSON ResourceId where   parseJSON = genericParseJSON (defaultOptions {unwrapUnaryRecords = True}) +-- | A complete resource data Resource = Resource {-     resId      :: Maybe ResourceId,-     resName    :: ResourceName,-     resType    :: Maybe Text,-     resUris    :: [Text],-     resScopes  :: [Scope],-     resOwner   :: Owner,+     resId                 :: Maybe ResourceId,+     resName               :: ResourceName,+     resType               :: Maybe Text,+     resUris               :: [Text],+     resScopes             :: [Scope],+     resOwner              :: Owner,      resOwnerManagedAccess :: Bool,-     resAttributes :: [Attribute]+     resAttributes         :: [Attribute]   } deriving (Generic, Show)  instance FromJSON Resource where@@ -274,6 +273,7 @@             "ownerManagedAccess" .= toJSON uma,             "attributes"         .= object (map (\(Attribute name vals) -> name .= toJSON vals) attrs)] +-- | A resource attribute data Attribute = Attribute {   attName   :: Text,   attValues :: [Text]