Expose all claims via sql postgrest.claims

This commit is contained in:
Joe Nelson
2016-03-11 20:51:22 -08:00
parent 508d722fb2
commit f67e195f76
5 changed files with 77 additions and 36 deletions
+6
View File
@@ -40,6 +40,8 @@ executable postgrest
, http-types , http-types
, interpolatedstring-perl6 , interpolatedstring-perl6
, jwt , jwt
, lens >=3.8 && < 5.0
, lens-aeson >= 1.0.0.0 && < 1.1.0.0
, mtl , mtl
, optparse-applicative >= 0.11 && < 0.13 , optparse-applicative >= 0.11 && < 0.13
, parsec , parsec
@@ -93,6 +95,8 @@ library
, http-types , http-types
, interpolatedstring-perl6 , interpolatedstring-perl6
, jwt , jwt
, lens
, lens-aeson
, mtl , mtl
, optparse-applicative , optparse-applicative
, parsec , parsec
@@ -175,6 +179,8 @@ Test-Suite spec
, http-types , http-types
, interpolatedstring-perl6 , interpolatedstring-perl6
, jwt , jwt
, lens
, lens-aeson
, monad-control , monad-control
, mtl , mtl
, optparse-applicative , optparse-applicative
+23 -19
View File
@@ -18,12 +18,13 @@ module PostgREST.Auth (
, tokenJWT , tokenJWT
) where ) where
import Control.Monad (join) import Control.Lens
import Data.Aeson (Value (..), parseJSON) import Data.Aeson (Value (..), parseJSON, toJSON)
import Data.Aeson.Lens
import Data.Aeson.Types (parseMaybe, emptyObject, emptyArray) import Data.Aeson.Types (parseMaybe, emptyObject, emptyArray)
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import Data.Vector as V (null, head) import qualified Data.Vector as V
import Data.Map as M (toList) import qualified Data.HashMap.Strict as M
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Data.Monoid ((<>)) import Data.Monoid ((<>))
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
@@ -39,12 +40,12 @@ import qualified Web.JWT as JWT
this one is mapped to a SET ROLE statement. this one is mapped to a SET ROLE statement.
In case there is any problem decoding the JWT it returns Nothing. In case there is any problem decoding the JWT it returns Nothing.
-} -}
claimsToSQL :: JWT.ClaimsMap -> [BS.ByteString] claimsToSQL :: M.HashMap Text Value -> [BS.ByteString]
claimsToSQL = map setVar . toList claimsToSQL = map setVar . M.toList
where where
setVar ("role", String val) = setRole val setVar ("role", String val) = setRole val
setVar (k, val) = "set local postgrest.claims." <> cs (pgFmtIdent k) <> setVar (k, val) = "set local " <> cs (pgFmtIdent $ "postgrest.claims." <> k)
" = " <> cs (valueToVariable val) <> ";" <> " = " <> cs (valueToVariable val) <> ";"
valueToVariable = pgFmtLit . unquoted valueToVariable = pgFmtLit . unquoted
{-| {-|
@@ -52,19 +53,22 @@ claimsToSQL = map setVar . toList
returns a map of JWT claims returns a map of JWT claims
In case there is any problem decoding the JWT it returns Nothing. In case there is any problem decoding the JWT it returns Nothing.
-} -}
jwtClaims :: JWT.Secret -> Text -> NominalDiffTime -> Maybe JWT.ClaimsMap
jwtClaims :: JWT.Secret -> Text -> NominalDiffTime -> Either Text (M.HashMap Text Value)
jwtClaims secret input time = jwtClaims secret input time =
case join $ claim JWT.exp of case mClaims of
Just expires -> Nothing -> Right M.empty
if JWT.secondsSinceEpoch expires > time Just claims -> do
then customClaims let mExp = claims ^? key "exp" . _Integer
else Nothing expired = fromMaybe False $ (<= time) . fromInteger <$> mExp
_ -> customClaims if expired
then Left "JWT expired"
else Right (value2map claims)
where where
decoded = JWT.decodeAndVerifySignature secret input mClaims = toJSON . JWT.claims <$> JWT.decodeAndVerifySignature secret input
claim :: (JWT.JWTClaimsSet -> a) -> Maybe a value2map (Object o) = o
claim prop = prop . JWT.claims <$> decoded value2map _ = M.empty
customClaims = claim JWT.unregisteredClaims
{-| Receives the name of a role and returns a SET ROLE statement -} {-| Receives the name of a role and returns a SET ROLE statement -}
setRole :: Text -> BS.ByteString setRole :: Text -> BS.ByteString
+13 -13
View File
@@ -3,6 +3,7 @@
module PostgREST.Middleware where module PostgREST.Middleware where
import qualified Data.HashMap.Strict as M
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Data.Text import Data.Text
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
@@ -22,33 +23,32 @@ import PostgREST.Auth (setRole, jwtClaims, claimsToSQL)
import PostgREST.Config (AppConfig (..), corsPolicy) import PostgREST.Config (AppConfig (..), corsPolicy)
import PostgREST.Error (errResponse) import PostgREST.Error (errResponse)
import Prelude hiding(concat) import Prelude hiding (concat, null)
import qualified Data.Map.Lazy as M
runWithClaims :: AppConfig -> NominalDiffTime -> runWithClaims :: AppConfig -> NominalDiffTime ->
(Request -> H.Transaction Response) -> (Request -> H.Transaction Response) ->
Request -> H.Transaction Response Request -> H.Transaction Response
runWithClaims conf time app req = do runWithClaims conf time app req = do
H.sql setAnon H.sql setAnon
case split (== ' ') (cs auth) of let tokenStr = case split (== ' ') (cs auth) of
("Bearer" : tokenStr : _) -> ("Bearer" : t : _) -> t
case jwtClaims jwtSecret tokenStr time of _ -> ""
Just claims -> eClaims = jwtClaims jwtSecret tokenStr time
if M.member "role" claims case eClaims of
then do Left e -> clientErr e
Right claims ->
if M.null claims && not (null tokenStr)
then clientErr "Invalid JWT"
else do
mapM_ H.sql $ claimsToSQL claims mapM_ H.sql $ claimsToSQL claims
app req app req
else invalidJWT
_ -> invalidJWT
_ -> app req
where where
hdrs = requestHeaders req hdrs = requestHeaders req
jwtSecret = configJwtSecret conf jwtSecret = configJwtSecret conf
auth = fromMaybe "" $ lookup hAuthorization hdrs auth = fromMaybe "" $ lookup hAuthorization hdrs
anon = cs $ configAnonRole conf anon = cs $ configAnonRole conf
setAnon = setRole anon setAnon = setRole anon
invalidJWT = return $ errResponse status400 "Invalid JWT" clientErr = return . errResponse status400
unsupportedAccept :: Application -> Application unsupportedAccept :: Application -> Application
unsupportedAccept app req respond = unsupportedAccept app req respond =
+8
View File
@@ -32,6 +32,14 @@ spec = describe "authorization" $ do
, matchHeaders = ["Content-Type" <:> "application/json"] , matchHeaders = ["Content-Type" <:> "application/json"]
} }
it "sql functions can read custom and standard claims variables" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmdW4iLCJqdGkiOiJmb28iLCJuYmYiOjEzMDA4MTkzODAsImV4cCI6OTk5OTk5OTk5OSwiaHR0cDovL3Bvc3RncmVzdC5jb20vZm9vIjp0cnVlLCJpc3MiOiJqb2UiLCJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWF0IjoxMzAwODE5MzgwLCJhdWQiOiJldmVyeW9uZSJ9.AQmCA7CMScvfaDRMqRPeUY6eNf--69gpW-kxaWfq9X0"
request methodPost "/rpc/reveal_big_jwt" [auth] "{}"
`shouldRespondWith` [json| [
{"sub":"fun", "jti":"foo", "nbf":1300819380, "exp":9999999999,
"http://postgrest.com/foo":true, "iss":"joe", "iat":1300819380,
"aud":"everyone"}] |]
it "allows users with permissions to see their tables" $ do it "allows users with permissions to see their tables" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0" let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
request methodGet "/authors_only" [auth] "" request methodGet "/authors_only" [auth] ""
+23
View File
@@ -214,6 +214,29 @@ SELECT 'joe'::text as iss, 'fun'::text as sub, 'everyone'::text as aud,
$$; $$;
--
-- Name: reveal_big_jwt(); Type: FUNCTION; Schema: test; Owner: -
--
CREATE FUNCTION reveal_big_jwt() RETURNS TABLE (
iss text, sub text, aud text, exp bigint,
nbf bigint, iat bigint, jti text, "http://postgrest.com/foo" boolean
)
LANGUAGE sql SECURITY DEFINER
AS $$
SELECT current_setting('postgrest.claims.iss') as iss,
current_setting('postgrest.claims.sub') as sub,
current_setting('postgrest.claims.aud') as aud,
current_setting('postgrest.claims.exp')::bigint as exp,
current_setting('postgrest.claims.nbf')::bigint as nbf,
current_setting('postgrest.claims.iat')::bigint as iat,
current_setting('postgrest.claims.jti') as jti,
-- role is not included in the claims list
current_setting('postgrest.claims.http://postgrest.com/foo')::boolean
as "http://postgrest.com/foo";
$$;
-- --
-- Name: problem(); Type: FUNCTION; Schema: test; Owner: - -- Name: problem(); Type: FUNCTION; Schema: test; Owner: -
-- --