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 +56/−6
- examples/Main.hs +55/−0
- keycloak-hs.cabal +9/−2
- src/Keycloak/Client.hs +23/−17
- src/Keycloak/Types.hs +23/−9
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