packages feed

keycloak-hs 0.2.0 → 1.0.0

raw patch · 5 files changed

+166/−34 lines, 5 filesdep +keycloak-hsdep ~basenew-component:exe:example

Dependencies added: keycloak-hs

Dependency ranges changed: base

Files

README.md view
@@ -1,10 +1,8 @@-keycloak-hs+Keycloak-hs =========== -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 is an Haskell library for connecting to [Keycloak](https://www.keycloak.org/).+Keycloak allows to authenticate users and protect API resources.  Install =======@@ -17,4 +15,56 @@ Tutorial ======== -TBD+In this tutorial we'll learn how to use Keycloak-hs with a [small example](./example/Main.hs).+First you should install and run Keycloak: [follow this tutorial](https://www.keycloak.org/docs/latest/getting_started/index.html).+In Keycloak admin panel, create the following:+- a realm named "demo"+- a user "demo" with password "demo"+- a client named "demo".++In the client "demo":+- change "Access Type" to confidential+- turn "Authorization Enabled" ON.++A new "Authorization" tab should appear.++Authorizations+--------------++Let's set up some authorization policies in order to demonstrate the capacity of Keycloak-hs.+We want to authorize our user "demo" to "view" any resource.+Frist go in the new "Authorization" tab that appeared.++Create a new Scope in the "Authorization Scopes" tab:+- Name it "view".++Create a new "User" policy in the "Policies" tab with the following settings:+- Name it "Demo user have access".+- Select user "demo" in the drop box.+- Logic should be positive.++Create a new scope-based permission in the "Permissions" tab:+- Name it "View resources".+- Select "view" in Scopes.+- Select your previous policy "Demo user have access" in "Apply Policy".++That's it for the confguration of Keycloak. Keycloak is very complex, so you'll have fun exploring all the possibilities ;)++Example code+-----------++The folder example contains an [exemple of usage](./example/Main.hs).+You should first input your "client secret", that can be found in the demo client "Credentials" tab in Keycloak admin panel.++Then run the example:+```+stack run example+```++The example first create a "client" token, necessary to create a resource in Keycloak.+It then create a Resourse, with a name, an optional type, URIs, scopes, owner and attributes.++We can then check if our user can access this resource, according to policies.+Finally, the example shows how to retrieve all permissions for a user.++Enjoy!
+ examples/Main.hs view
@@ -0,0 +1,55 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Keycloak+import Control.Monad+import Control.Monad.IO.Class++-- Kecyloak configuration.+kcConfig :: KCConfig+kcConfig = KCConfig {+  _baseUrl       = "http://localhost:8080/auth",+  _realm         = "demo",+  _clientId      = "demo",+  _clientSecret  = "4270ce82-4a8f-4d89-9ea9-d9b28c3bab3e"}++main :: IO ()+main = void $ flip runKeycloak kcConfig $ do+  +  liftIO $ putStrLn "Getting Client token"++  -- * We first get a client token, used to create resources +  clientToken <- getClientAuthToken++  liftIO $ putStrLn "Creating resource"++  -- * We will than create a resource to be protected in Keycloak+  let res = Resource {+         resId      = Nothing,+         resName    = "MyResource",+         resType    = Nothing,+         resUris    = [],+         resScopes  = [Scope Nothing (ScopeName "view")],+         resOwner   = Owner Nothing (Just "demo"),+         resOwnerManagedAccess = False,+         resAttributes = []}+  resId <- createResource res clientToken  ++  liftIO $ putStrLn "Getting User token"++  -- * Let's get a token for a specific user+  userToken <- getUserAuthToken "demo" "demo"++  -- * Can I access this resource?+  isAuth <- isAuthorized resId (ScopeName "view") userToken++  liftIO $ putStrLn $ "User 'demo' can access resource 'demo': " ++ (show isAuth)++  -- We can also retrieve all the permissions for our user.+  perms <- getPermissions [PermReq Nothing [ScopeName "view"]] userToken++  liftIO $ putStrLn $ "All permissions: " ++ (show perms)++  --resources can be deleted+  --deleteResource resId clientToken
keycloak-hs.cabal view
@@ -1,6 +1,5 @@-cabal-version: 1.12 name:           keycloak-hs-version:        0.2.0+version:        1.0.0 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@@ -10,6 +9,7 @@ license:        BSD3 license-file:   LICENSE build-type:     Simple+cabal-version:  1.12 extra-source-files:     README.md     ChangeLog.md@@ -49,4 +49,11 @@     , safe   default-language: Haskell2010 +Executable example+  hs-source-dirs:      examples+  Main-Is:             Main.hs+  ghc-options:        -threaded -Wall+  build-depends:       base+                     , keycloak-hs+  default-language:    Haskell2010 
src/Keycloak/Client.hs view
@@ -14,6 +14,7 @@ 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@@ -34,17 +35,6 @@  -- * Permissions --- | 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) 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 ()- -- | Returns true id the resource is authorized under the given scope. isAuthorized :: ResourceId -> ScopeName -> Token -> Keycloak Bool isAuthorized res scope tok = do@@ -55,14 +45,14 @@     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+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 (\s -> "permission" := ("#" <> s)) scopes+             "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@@ -71,7 +61,23 @@       debug $ "Keycloak parse error: " ++ (show err2)        throwError $ ParseError $ pack (show err2) +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@@ -154,8 +160,8 @@ deleteAllResources tok = do   debug "Deleting all Keycloak resources..."   ids <- getAllResourceIds-  mapM_ (\rid -> deleteResource rid tok) ids-  debug $ "Deleted " ++ (show $ L.length ids) ++ " resources from Keycloak"+  res <- mapM (\rid -> try $ deleteResource rid tok) ids+  debug $ "Deleted " ++ (show $ L.length $ rights res) ++ " resources out of " ++ (show $ L.length ids)  getResource :: ResourceId -> Keycloak Resource getResource (ResourceId rid) = do
src/Keycloak/Types.hs view
@@ -122,7 +122,7 @@   tokenType         :: Text,   notBeforePolicy   :: Int,   sessionState      :: Text,-  scope             :: Text} deriving (Show, Eq)+  tokenScope        :: Text} deriving (Show, Eq)  instance FromJSON TokenRep where   parseJSON (Object v) = TokenRep <$> v .: "access_token"@@ -134,11 +134,18 @@                                   <*> v .: "session_state"                                   <*> v .: "scope" --- * Permission+-- * Permissions  -- | Scope name-type ScopeName = Text+newtype ScopeName = ScopeName {unScopeName :: Text} deriving (Show, Eq, Generic, Ord) +--JSON instances+instance ToJSON ScopeName where+  toJSON = genericToJSON (defaultOptions {unwrapUnaryRecords = True})++instance FromJSON ScopeName where+  parseJSON = genericParseJSON (defaultOptions {unwrapUnaryRecords = True})+ -- | Scope Id newtype ScopeId = ScopeId {unScopeId :: Text} deriving (Show, Eq, Generic) @@ -163,18 +170,25 @@  -- | Keycloak permission on a resource data Permission = Permission -  { rsname :: ResourceName,-    rsid   :: ResourceId,-    scopes :: [ScopeName]+  { permRsid   :: Maybe ResourceId,   -- Resource ID, can be Nothing in case of scope-only permission request+    permRsname :: Maybe ResourceName, -- Resrouce Name+    permScopes :: [ScopeName]         -- Scopes that are accessible Non empty   } deriving (Generic, Show, Eq)  instance ToJSON Permission where-  toJSON = genericToJSON defaultOptions {omitNothingFields = True}+  toJSON = genericToJSON defaultOptions {fieldLabelModifier = unCapitalize . drop 4, omitNothingFields = True}  instance FromJSON Permission where-  parseJSON = genericParseJSON defaultOptions+  parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = unCapitalize . drop 4} +-- | permission request+data PermReq = PermReq +  { permReqResourceId :: Maybe ResourceId, -- Requested ressource Ids. Nothing means "All resources".+    permReqScopes     :: [ScopeName]       -- Scopes requested. [] means "all scopes".+  } deriving (Generic, Show, Eq, Ord) ++ -- * User  type Username = Text@@ -235,7 +249,7 @@ type ResourceType = Text  -- | A resource Id-newtype ResourceId = ResourceId {unResId :: Text} deriving (Show, Eq, Generic)+newtype ResourceId = ResourceId {unResId :: Text} deriving (Show, Eq, Generic, Ord)  -- JSON instances instance ToJSON ResourceId where