Support asymmetric JWK (#919)

This commit is contained in:
Joe Nelson
2017-08-09 08:42:00 -05:00
committed by GitHub
parent 2243f4b653
commit 0ed4215a0d
28 changed files with 193 additions and 154 deletions
+1
View File
@@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Added ### Added
- #567, Support more JWT signing algorithms, including RSA - @begriffs
- #889, Allow more than two conditions in a single and/or - @steve-chavez - #889, Allow more than two conditions in a single and/or - @steve-chavez
- #883, Binary output support for RPC - @steve-chavez - #883, Binary output support for RPC - @steve-chavez
- #885, Postgres COMMENTs on SCHEMA/TABLE/COLUMN are used for OpenAPI - @ldesgoui - #885, Postgres COMMENTs on SCHEMA/TABLE/COLUMN are used for OpenAPI - @ldesgoui
+10 -17
View File
@@ -13,8 +13,6 @@ import PostgREST.OpenAPI (isMalformedProxyUri)
import PostgREST.Types (DbStructure, Schema) import PostgREST.Types (DbStructure, Schema)
import Protolude import Protolude
import Control.AutoUpdate (defaultUpdateSettings,
mkAutoUpdate, updateAction)
import Control.Retry (RetryStatus, capDelay, import Control.Retry (RetryStatus, capDelay,
exponentialBackoff, exponentialBackoff,
retrying, rsPreviousDelay) retrying, rsPreviousDelay)
@@ -25,7 +23,6 @@ import Data.String (IsString (..))
import Data.Text (pack, replace, stripPrefix, strip) import Data.Text (pack, replace, stripPrefix, strip)
import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import Data.Text.IO (hPutStrLn, readFile) import Data.Text.IO (hPutStrLn, readFile)
import Data.Time.Clock.POSIX (getPOSIXTime)
import qualified Hasql.Decoders as HD import qualified Hasql.Decoders as HD
import qualified Hasql.Encoders as HE import qualified Hasql.Encoders as HE
import qualified Hasql.Pool as P import qualified Hasql.Pool as P
@@ -72,7 +69,7 @@ isServerVersionSupported = do
4. If 2 or 3 fail to give their result it means the connection is down so it 4. If 2 or 3 fail to give their result it means the connection is down so it
goes back to 1, otherwise it finishes his work successfully. goes back to 1, otherwise it finishes his work successfully.
-} -}
connectionWorker connectionWorker
:: ThreadId -- ^ This thread is killed if 'isServerVersionSupported' returns false :: ThreadId -- ^ This thread is killed if 'isServerVersionSupported' returns false
-> P.Pool -- ^ The PostgreSQL connection pool -> P.Pool -- ^ The PostgreSQL connection pool
-> Schema -- ^ Schema PostgREST is serving up -> Schema -- ^ Schema PostgREST is serving up
@@ -144,7 +141,7 @@ connectingSucceeded pool =
main :: IO () main :: IO ()
main = do main = do
-- --
-- LineBuffering: the entire output buffer is flushed whenever a newline is -- LineBuffering: the entire output buffer is flushed whenever a newline is
-- output, the buffer overflows, a hFlush is issued or the handle is closed -- output, the buffer overflows, a hFlush is issued or the handle is closed
-- --
-- NoBuffering: output is written immediately and never stored in the buffer -- NoBuffering: output is written immediately and never stored in the buffer
@@ -166,7 +163,7 @@ main = do
. setTimeout 3600 $ . setTimeout 3600 $
defaultSettings defaultSettings
-- --
-- Checks that the provided proxy uri is formated correctly, -- Checks that the provided proxy uri is formated correctly,
-- does not test if it works here. -- does not test if it works here.
when (isMalformedProxyUri $ toS <$> proxy) $ when (isMalformedProxyUri $ toS <$> proxy) $
panic panic
@@ -210,18 +207,15 @@ main = do
) Nothing ) Nothing
void $ installHandler sigHUP ( void $ installHandler sigHUP (
Catch $ connectionWorker Catch $ connectionWorker
mainTid mainTid
pool pool
(configSchema conf) (configSchema conf)
refDbStructure refDbStructure
refIsWorkerOn refIsWorkerOn
) Nothing ) Nothing
#endif #endif
--
-- ask for the OS time at most once per second
getTime <-
mkAutoUpdate defaultUpdateSettings {updateAction = getPOSIXTime}
-- --
-- run the postgrest application -- run the postgrest application
runSettings appSettings $ runSettings appSettings $
@@ -229,7 +223,6 @@ main = do
conf conf
refDbStructure refDbStructure
pool pool
getTime
(connectionWorker (connectionWorker
mainTid mainTid
pool pool
@@ -257,7 +250,7 @@ loadSecretFile conf = extractAndTransform mSecret
mSecret = decodeUtf8 <$> configJwtSecret conf mSecret = decodeUtf8 <$> configJwtSecret conf
isB64 = configJwtSecretIsBase64 conf isB64 = configJwtSecretIsBase64 conf
-- --
-- The Text (variable name secret) here is mSecret from above which is the JWT -- The Text (variable name secret) here is mSecret from above which is the JWT
-- decoded as Utf8 -- decoded as Utf8
-- --
-- stripPrefix: Return the suffix of the second string if its prefix matches -- stripPrefix: Return the suffix of the second string if its prefix matches
+4 -7
View File
@@ -29,14 +29,12 @@ executable postgrest
-rtsopts -rtsopts
"-with-rtsopts=-N -I2" "-with-rtsopts=-N -I2"
default-language: Haskell2010 default-language: Haskell2010
build-depends: auto-update build-depends: base
, base
, hasql , hasql
, hasql-pool , hasql-pool
, postgrest , postgrest
, protolude , protolude
, text , text
, time
, warp , warp
, bytestring , bytestring
, base64-bytestring , base64-bytestring
@@ -52,6 +50,7 @@ library
build-depends: aeson build-depends: aeson
, ansi-wl-pprint , ansi-wl-pprint
, base >= 4.8 && < 6 , base >= 4.8 && < 6
, base64-bytestring
, bytestring , bytestring
, case-insensitive , case-insensitive
, cassava , cassava
@@ -67,7 +66,7 @@ library
, http-types , http-types
, insert-ordered-containers , insert-ordered-containers
, interpolatedstring-perl6 , interpolatedstring-perl6
, jwt , jose
, lens , lens
, lens-aeson , lens-aeson
, network-uri , network-uri
@@ -80,7 +79,6 @@ library
, scientific , scientific
, swagger2 , swagger2
, text , text
, time
, unordered-containers , unordered-containers
, vector , vector
, wai , wai
@@ -113,6 +111,7 @@ Test-Suite spec
Hs-Source-Dirs: test Hs-Source-Dirs: test
Main-Is: Main.hs Main-Is: Main.hs
Other-Modules: Feature.AuthSpec Other-Modules: Feature.AuthSpec
, Feature.AsymmetricJwtSpec
, Feature.BinaryJwtSecretSpec , Feature.BinaryJwtSecretSpec
, Feature.ConcurrentSpec , Feature.ConcurrentSpec
, Feature.CorsSpec , Feature.CorsSpec
@@ -132,7 +131,6 @@ Test-Suite spec
Build-Depends: aeson Build-Depends: aeson
, aeson-qq , aeson-qq
, async , async
, auto-update
, base , base
, bytestring , bytestring
, base64-bytestring , base64-bytestring
@@ -156,7 +154,6 @@ Test-Suite spec
, process , process
, protolude , protolude
, regex-tdfa , regex-tdfa
, time
, transformers-base , transformers-base
, wai , wai
, wai-extra , wai-extra
+9 -12
View File
@@ -11,7 +11,6 @@ import qualified Data.ByteString.Char8 as BS
import Data.Maybe import Data.Maybe
import Data.IORef (IORef, readIORef) import Data.IORef (IORef, readIORef)
import Data.Text (intercalate) import Data.Text (intercalate)
import Data.Time.Clock.POSIX (POSIXTime)
import qualified Hasql.Pool as P import qualified Hasql.Pool as P
import qualified Hasql.Transaction as HT import qualified Hasql.Transaction as HT
@@ -22,7 +21,6 @@ import Network.HTTP.Types.Status
import Network.HTTP.Types.URI (renderSimpleQuery) import Network.HTTP.Types.URI (renderSimpleQuery)
import Network.Wai import Network.Wai
import Network.Wai.Middleware.RequestLogger (logStdout) import Network.Wai.Middleware.RequestLogger (logStdout)
import Web.JWT (binarySecret)
import qualified Data.Vector as V import qualified Data.Vector as V
import qualified Hasql.Transaction as H import qualified Hasql.Transaction as H
@@ -35,7 +33,7 @@ import PostgREST.ApiRequest ( ApiRequest(..), ContentType(..)
, mutuallyAgreeable , mutuallyAgreeable
, userApiRequest , userApiRequest
) )
import PostgREST.Auth (jwtClaims, containsRole) import PostgREST.Auth (jwtClaims, containsRole, parseJWK)
import PostgREST.Config (AppConfig (..)) import PostgREST.Config (AppConfig (..))
import PostgREST.DbStructure import PostgREST.DbStructure
import PostgREST.DbRequestBuilder( readRequest import PostgREST.DbRequestBuilder( readRequest
@@ -63,13 +61,12 @@ import Data.Function (id)
import Protolude hiding (intercalate, Proxy) import Protolude hiding (intercalate, Proxy)
import Safe (headMay) import Safe (headMay)
postgrest :: AppConfig -> IORef (Maybe DbStructure) -> P.Pool -> IO POSIXTime -> postgrest :: AppConfig -> IORef (Maybe DbStructure) -> P.Pool -> IO () -> Application
IO () -> Application postgrest conf refDbStructure pool worker =
postgrest conf refDbStructure pool getTime worker = let middle = (if configQuiet conf then id else logStdout) . defaultMiddle
let middle = (if configQuiet conf then id else logStdout) . defaultMiddle in jwtSecret = parseJWK <$> configJwtSecret conf in
middle $ \ req respond -> do middle $ \ req respond -> do
time <- getTime
body <- strictRequestBody req body <- strictRequestBody req
maybeDbStructure <- readIORef refDbStructure maybeDbStructure <- readIORef refDbStructure
case maybeDbStructure of case maybeDbStructure of
@@ -78,9 +75,9 @@ postgrest conf refDbStructure pool getTime worker =
response <- case userApiRequest (configSchema conf) req body of response <- case userApiRequest (configSchema conf) req body of
Left err -> return $ apiRequestError err Left err -> return $ apiRequestError err
Right apiRequest -> do Right apiRequest -> do
let jwtSecret = binarySecret <$> configJwtSecret conf eClaims <- jwtClaims jwtSecret (toS $ iJWT apiRequest)
eClaims = jwtClaims jwtSecret (iJWT apiRequest) time
authed = containsRole eClaims let authed = containsRole eClaims
handleReq = runWithClaims conf eClaims (app dbStructure conf) apiRequest handleReq = runWithClaims conf eClaims (app dbStructure conf) apiRequest
txMode = transactionMode dbStructure txMode = transactionMode dbStructure
(iTarget apiRequest) (iAction apiRequest) (iTarget apiRequest) (iAction apiRequest)
@@ -324,7 +321,7 @@ responseContentTypeOrError accepts action = serves contentTypesForRequest accept
case mutuallyAgreeable sProduces cAccepts of case mutuallyAgreeable sProduces cAccepts of
Nothing -> do Nothing -> do
let failed = intercalate ", " $ map (toS . toMime) cAccepts let failed = intercalate ", " $ map (toS . toMime) cAccepts
Left $ simpleError status415 $ Left $ simpleError status415 [] $
"None of these Content-Types are available: " <> failed "None of these Content-Types are available: " <> failed
Just ct -> Right ct Just ct -> Right ct
+51 -39
View File
@@ -14,63 +14,48 @@ very simple authentication system inside the PostgreSQL database.
module PostgREST.Auth ( module PostgREST.Auth (
containsRole containsRole
, jwtClaims , jwtClaims
, tokenJWT
, JWTAttempt(..) , JWTAttempt(..)
, parseJWK
) where ) where
import Protolude import Protolude hiding ((&))
import Control.Lens import Control.Lens
import Data.Aeson (Value (..), parseJSON, toJSON) import Data.Aeson (Value (..), decode, toJSON)
import Data.Aeson.Lens import qualified Data.ByteString.Lazy as BL
import Data.Aeson.Types (parseMaybe, emptyObject, emptyArray)
import qualified Data.Vector as V
import qualified Data.HashMap.Strict as M import qualified Data.HashMap.Strict as M
import Data.Maybe (fromJust)
import Data.Time.Clock (NominalDiffTime)
import qualified Web.JWT as JWT
import Crypto.JOSE.Compact
import Crypto.JOSE.JWK
import Crypto.JOSE.JWS
import Crypto.JOSE.Types
import Crypto.JWT
{-| {-|
Possible situations encountered with client JWTs Possible situations encountered with client JWTs
-} -}
data JWTAttempt = JWTExpired data JWTAttempt = JWTInvalid JWTError
| JWTInvalid
| JWTMissingSecret | JWTMissingSecret
| JWTClaims (M.HashMap Text Value) | JWTClaims (M.HashMap Text Value)
deriving Eq deriving (Eq, Show)
{-| {-|
Receives the JWT secret (from config) and a JWT and returns a map Receives the JWT secret (from config) and a JWT and returns a map
of JWT claims. of JWT claims.
-} -}
jwtClaims :: Maybe JWT.Secret -> Text -> NominalDiffTime -> JWTAttempt jwtClaims :: Maybe JWK -> BL.ByteString -> IO JWTAttempt
jwtClaims _ "" _ = JWTClaims M.empty jwtClaims _ "" = return $ JWTClaims M.empty
jwtClaims secret jwt time = jwtClaims secret payload =
case secret of case secret of
Nothing -> JWTMissingSecret Nothing -> return JWTMissingSecret
Just s -> Just jwk -> do
let mClaims = toJSON . JWT.claims <$> JWT.decodeAndVerifySignature s jwt in let validation = defaultJWTValidationSettings
case isExpired <$> mClaims of eJwt <- runExceptT $ do
Just True -> JWTExpired jwt <- decodeCompact payload
Nothing -> JWTInvalid validateJWSJWT validation jwk jwt
Just False -> JWTClaims $ value2map $ fromJust mClaims return jwt
where return $ case eJwt of
isExpired claims = Left e -> JWTInvalid e
let mExp = claims ^? key "exp" . _Integer Right jwt -> JWTClaims . claims2map . jwtClaimsSet $ jwt
in fromMaybe False $ (<= time) . fromInteger <$> mExp
value2map (Object o) = o
value2map _ = M.empty
{-|
Receives the JWT secret (from config) and a JWT and a JSON value
and returns a signed JWT.
-}
tokenJWT :: JWT.Secret -> Value -> Text
tokenJWT secret (Array arr) =
let obj = if V.null arr then emptyObject else V.head arr
jcs = parseMaybe parseJSON obj :: Maybe JWT.JWTClaimsSet in
JWT.encodeSigned JWT.HS256 secret $ fromMaybe JWT.def jcs
tokenJWT secret _ = tokenJWT secret emptyArray
{-| {-|
Whether a response from jwtClaims contains a role claim Whether a response from jwtClaims contains a role claim
@@ -78,3 +63,30 @@ tokenJWT secret _ = tokenJWT secret emptyArray
containsRole :: JWTAttempt -> Bool containsRole :: JWTAttempt -> Bool
containsRole (JWTClaims claims) = M.member "role" claims containsRole (JWTClaims claims) = M.member "role" claims
containsRole _ = False containsRole _ = False
{-|
Internal helper used to turn JWT ClaimSet into something
easier to work with
-}
claims2map :: ClaimsSet -> M.HashMap Text Value
claims2map = val2map . toJSON
where
val2map (Object o) = o
val2map _ = M.empty
parseJWK :: ByteString -> JWK
parseJWK str =
fromMaybe (hs256jwk str) (decode (toS str) :: Maybe JWK)
{-|
Internal helper to generate HMAC-SHA256. When the jwt key in the
config file is a simple string rather than a JWK object, we'll
apply this function to it.
-}
hs256jwk :: ByteString -> JWK
hs256jwk key =
fromKeyMaterial km
& jwkUse .~ Just Sig
& jwkAlg .~ (Just $ JWSAlg HS256)
where
km = OctKeyMaterial (OctKeyParameters Oct (Base64Octets key))
+13 -9
View File
@@ -18,12 +18,15 @@ import qualified Data.Aeson as JSON
import Data.Text (unwords) import Data.Text (unwords)
import qualified Hasql.Pool as P import qualified Hasql.Pool as P
import qualified Hasql.Session as H import qualified Hasql.Session as H
import Network.HTTP.Types.Header
import qualified Network.HTTP.Types.Status as HT import qualified Network.HTTP.Types.Status as HT
import Network.Wai (Response, responseLBS) import Network.Wai (Response, responseLBS)
import PostgREST.Types import PostgREST.Types
apiRequestError :: ApiRequestError -> Response apiRequestError :: ApiRequestError -> Response
apiRequestError err = errorResponse status err apiRequestError err =
errorResponse status
[toHeader CTApplicationJSON] err
where where
status = status =
case err of case err of
@@ -35,13 +38,14 @@ apiRequestError err = errorResponse status err
InvalidRange -> HT.status416 InvalidRange -> HT.status416
UnknownRelation -> HT.status404 UnknownRelation -> HT.status404
simpleError :: HT.Status -> Text -> Response simpleError :: HT.Status -> [Header] -> Text -> Response
simpleError status message = simpleError status hdrs message =
errorResponse status $ JSON.object ["message" .= message] errorResponse status (toHeader CTApplicationJSON : hdrs) $
JSON.object ["message" .= message]
errorResponse :: JSON.ToJSON a => HT.Status -> a -> Response errorResponse :: JSON.ToJSON a => HT.Status -> [Header] -> a -> Response
errorResponse status e = errorResponse status hdrs e =
responseLBS status [toHeader CTApplicationJSON] $ encodeError e responseLBS status hdrs $ encodeError e
pgError :: Bool -> P.UsageError -> Response pgError :: Bool -> P.UsageError -> Response
pgError authed e = pgError authed e =
@@ -71,12 +75,12 @@ singularityError numRows =
binaryFieldError :: Response binaryFieldError :: Response
binaryFieldError = binaryFieldError =
simpleError HT.status406 (toS (toMime CTOctetStream) <> simpleError HT.status406 [] (toS (toMime CTOctetStream) <>
" requested but a single column was not selected") " requested but a single column was not selected")
connectionLostError :: Response connectionLostError :: Response
connectionLostError = connectionLostError =
simpleError HT.status503 "Database connection lost, retrying the connection." simpleError HT.status503 [] "Database connection lost, retrying the connection."
encodeError :: JSON.ToJSON a => a -> LByteString encodeError :: JSON.ToJSON a => a -> LByteString
encodeError = JSON.encode encodeError = JSON.encode
+11 -13
View File
@@ -4,13 +4,13 @@
module PostgREST.Middleware where module PostgREST.Middleware where
import Crypto.JWT
import Data.Aeson (Value (..)) import Data.Aeson (Value (..))
import qualified Data.HashMap.Strict as M import qualified Data.HashMap.Strict as M
import qualified Hasql.Transaction as H import qualified Hasql.Transaction as H
import Network.HTTP.Types.Status (unauthorized401, status500) import Network.HTTP.Types.Status (unauthorized401, status500)
import Network.Wai (Application, Response, import Network.Wai (Application, Response)
responseLBS)
import Network.Wai.Middleware.Cors (cors) import Network.Wai.Middleware.Cors (cors)
import Network.Wai.Middleware.Gzip (def, gzip) import Network.Wai.Middleware.Gzip (def, gzip)
import Network.Wai.Middleware.Static (only, staticPolicy) import Network.Wai.Middleware.Static (only, staticPolicy)
@@ -19,7 +19,6 @@ import PostgREST.ApiRequest (ApiRequest(..))
import PostgREST.Auth (JWTAttempt(..)) import PostgREST.Auth (JWTAttempt(..))
import PostgREST.Config (AppConfig (..), corsPolicy) import PostgREST.Config (AppConfig (..), corsPolicy)
import PostgREST.Error (simpleError) import PostgREST.Error (simpleError)
import PostgREST.Types (ContentType (..), toHeader)
import PostgREST.QueryBuilder (pgFmtLit, unquoted, pgFmtEnvVar) import PostgREST.QueryBuilder (pgFmtLit, unquoted, pgFmtEnvVar)
import Protolude hiding (concat, null) import Protolude hiding (concat, null)
@@ -29,9 +28,9 @@ runWithClaims :: AppConfig -> JWTAttempt ->
ApiRequest -> H.Transaction Response ApiRequest -> H.Transaction Response
runWithClaims conf eClaims app req = runWithClaims conf eClaims app req =
case eClaims of case eClaims of
JWTExpired -> return $ unauthed "JWT expired" JWTInvalid JWTExpired -> return $ unauthed "JWT expired"
JWTInvalid -> return $ unauthed "JWT invalid" JWTInvalid e -> return $ unauthed $ show e
JWTMissingSecret -> return $ simpleError status500 "Server lacks JWT secret" JWTMissingSecret -> return $ simpleError status500 [] "Server lacks JWT secret"
JWTClaims claims -> do JWTClaims claims -> do
H.sql $ toS.mconcat $ setRoleSql ++ claimsSql ++ headersSql ++ cookiesSql H.sql $ toS.mconcat $ setRoleSql ++ claimsSql ++ headersSql ++ cookiesSql
mapM_ H.sql customReqCheck mapM_ H.sql customReqCheck
@@ -47,14 +46,13 @@ runWithClaims conf eClaims app req =
anon = String . toS $ configAnonRole conf anon = String . toS $ configAnonRole conf
customReqCheck = (\f -> "select " <> toS f <> "();") <$> configReqCheck conf customReqCheck = (\f -> "select " <> toS f <> "();") <$> configReqCheck conf
where where
unauthed message = responseLBS unauthorized401 unauthed message = simpleError
[ toHeader CTApplicationJSON unauthorized401
, ( "WWW-Authenticate" [( "WWW-Authenticate"
, "Bearer error=\"invalid_token\", " <> , "Bearer error=\"invalid_token\", " <>
"error_description=\"" <> message <> "\"" "error_description=" <> show message
) )]
] message
(toS $ "{\"message\":\""<>message<>"\"}")
defaultMiddle :: Application -> Application defaultMiddle :: Application -> Application
defaultMiddle = defaultMiddle =
+3 -2
View File
@@ -1,9 +1,10 @@
resolver: lts-8.5 resolver: lts-8.5
extra-deps: extra-deps:
- Ranged-sets-0.3.0
- hasql-pool-0.4.1
- configurator-ng-0.0.0.1 - configurator-ng-0.0.0.1
- critbit-0.2.0.0 - critbit-0.2.0.0
- hasql-pool-0.4.1
- jose-0.5.0.3
- Ranged-sets-0.3.0
ghc-options: ghc-options:
postgrest: -O2 -Werror -Wall -fwarn-identities -fno-warn-redundant-constraints postgrest: -O2 -Werror -Wall -fwarn-identities -fno-warn-redundant-constraints
nix: nix:
+21
View File
@@ -0,0 +1,21 @@
module Feature.AsymmetricJwtSpec where
-- {{{ Imports
import Test.Hspec
import Test.Hspec.Wai
import Network.HTTP.Types
import SpecHelper
import Network.Wai (Application)
import Protolude hiding (get)
-- }}}
spec :: SpecWith Application
spec = describe "server started with asymmetric JWK" $
-- this test will stop working 9999999999s after the UNIX EPOCH
it "succeeds with jwt token signed with an asymmetric key" $ do
let auth = authHeaderJWT "eyJhbGciOiJSUzI1NiJ9.eyJyb2xlIjogInBvc3RncmVzdF90ZXN0X2F1dGhvciJ9Cg.CBOYWDvqgAR0YYnZnyDGTQi6AJLc2Pds6_eV3YuBG6I36mj_h05eLhkEKNEDA5ZteMzCiY83P60rC_xtxVd7B6vo3BeF5uoanPS3rrbuHzKPwzsrgrD_CqvEuJ4n7Q9epkQiLsNkcexneENZDRqFjbwZx3DrXiCWwlK3Ytr5NAIGxmy0od-0xNpb2U1nXQyO_Q3mumWFViRt4tmFn_3goDHNKG3Ha_AzImfUNvHnWL78kAc4rbn15vLtWXD8PwtSnZaB4lY4V6RfsaW937srQsmRetvytM1i_bHBnjkjQLAqGbXPyItjtlXPs0uGNBadE8-wgkLtfmSCC4v2DjUthw"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
+17 -17
View File
@@ -28,7 +28,7 @@ spec = describe "authorization" $ do
} }
it "denies access to tables that postgrest_test_author does not own" $ it "denies access to tables that postgrest_test_author does not own" $
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0" in let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA" in
request methodGet "/private_table" [auth] "" request methodGet "/private_table" [auth] ""
`shouldRespondWith` [json| { `shouldRespondWith` [json| {
"hint":null, "hint":null,
@@ -42,41 +42,41 @@ spec = describe "authorization" $ do
it "returns jwt functions as jwt tokens" $ it "returns jwt functions as jwt tokens" $
request methodPost "/rpc/login" [single] request methodPost "/rpc/login" [single]
[json| { "id": "jdoe", "pass": "1234" } |] [json| { "id": "jdoe", "pass": "1234" } |]
`shouldRespondWith` [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xuYW1lIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.P2G9EVSVI22MWxXWFuhEYd9BZerLS1WDlqzdqplM15s"} |] `shouldRespondWith` [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xuYW1lIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.KO-0PGp_rU-utcDBP6qwdd-Th2Fk-ICVt01I7QtTDWs"} |]
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = [matchContentTypeSingular] , matchHeaders = [matchContentTypeSingular]
} }
it "sql functions can encode custom and standard claims" $ it "sql functions can encode custom and standard claims" $
request methodPost "/rpc/jwt_test" [single] "{}" request methodPost "/rpc/jwt_test" [single] "{}"
`shouldRespondWith` [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJqb2UiLCJzdWIiOiJmdW4iLCJhdWQiOiJldmVyeW9uZSIsImV4cCI6MTMwMDgxOTM4MCwibmJmIjoxMzAwODE5MzgwLCJpYXQiOjEzMDA4MTkzODAsImp0aSI6ImZvbyIsInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdCIsImh0dHA6Ly9wb3N0Z3Jlc3QuY29tL2ZvbyI6dHJ1ZX0.IHF16ZSU6XTbOnUWO8CCpUn2fJwt8P00rlYVyXQjpWc"} |] `shouldRespondWith` [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJqb2UiLCJzdWIiOiJmdW4iLCJhdWQiOiJldmVyeW9uZSIsImV4cCI6MTMwMDgxOTM4MCwibmJmIjoxMzAwODE5MzgwLCJpYXQiOjEzMDA4MTkzODAsImp0aSI6ImZvbyIsInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdCIsImh0dHA6Ly9wb3N0Z3Jlc3QuY29tL2ZvbyI6dHJ1ZX0.G2REtPnOQMUrVRDA9OnkPJTd8R0tf4wdYOlauh1E2Ek"} |]
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = [matchContentTypeSingular] , matchHeaders = [matchContentTypeSingular]
} }
it "sql functions can read custom and standard claims variables" $ do it "sql functions can read custom and standard claims variables" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmdW4iLCJqdGkiOiJmb28iLCJuYmYiOjEzMDA4MTkzODAsImV4cCI6OTk5OTk5OTk5OSwiaHR0cDovL3Bvc3RncmVzdC5jb20vZm9vIjp0cnVlLCJpc3MiOiJqb2UiLCJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWF0IjoxMzAwODE5MzgwLCJhdWQiOiJldmVyeW9uZSJ9.AQmCA7CMScvfaDRMqRPeUY6eNf--69gpW-kxaWfq9X0" let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmdW4iLCJqdGkiOiJmb28iLCJuYmYiOjEzMDA4MTkzODAsImV4cCI6OTk5OTk5OTk5OSwiaHR0cDovL3Bvc3RncmVzdC5jb20vZm9vIjp0cnVlLCJpc3MiOiJqb2UiLCJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWF0IjoxMzAwODE5MzgwfQ.V5fEpXfpb7feqwVqlcDleFdKu86bdwU2cBRT4fcMhXg"
request methodPost "/rpc/reveal_big_jwt" [auth] "{}" request methodPost "/rpc/reveal_big_jwt" [auth] "{}"
`shouldRespondWith` [str|[{"iss":"joe","sub":"fun","aud":"everyone","exp":9999999999,"nbf":1300819380,"iat":1300819380,"jti":"foo","http://postgrest.com/foo":true}]|] `shouldRespondWith` [str|[{"iss":"joe","sub":"fun","exp":9999999999,"nbf":1300819380,"iat":1300819380,"jti":"foo","http://postgrest.com/foo":true}]|]
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.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0"
request methodGet "/authors_only" [auth] "" request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200 `shouldRespondWith` 200
it "works with tokens which have extra fields" $ do it "works with tokens which have extra fields" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIiwia2V5MSI6InZhbHVlMSIsImtleTIiOiJ2YWx1ZTIiLCJrZXkzIjoidmFsdWUzIiwiYSI6MSwiYiI6MiwiYyI6M30.GfydCh-F4wnM379xs0n1zUgalwJIsb6YoBapCo8HlFk" let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIiwia2V5MSI6InZhbHVlMSIsImtleTIiOiJ2YWx1ZTIiLCJrZXkzIjoidmFsdWUzIiwiYSI6MSwiYiI6MiwiYyI6M30.b0eglDKYEmGi-hCvD-ddSqFl7vnDO5qkUaviaHXm3es"
request methodGet "/authors_only" [auth] "" request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200 `shouldRespondWith` 200
-- this test will stop working 9999999999s after the UNIX EPOCH -- this test will stop working 9999999999s after the UNIX EPOCH
it "succeeds with an unexpired token" $ do it "succeeds with an unexpired token" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.QaPPLWTuyydMu_q7H4noMT7Lk6P4muet1OpJXF6ofhc" let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.Dpss-QoLYjec5OTsOaAc3FNVsSjA89wACoV-0ra3ClA"
request methodGet "/authors_only" [auth] "" request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200 `shouldRespondWith` 200
it "fails with an expired token" $ do it "fails with an expired token" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NDY2NzgxNDksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.enk_qZ_u6gZsXY4R8bREKB_HNExRpM0lIWSLktk9JJQ" let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NDY2NzgxNDksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.f8__E6VQwYcDqwHmr9PG03uaZn8Zh1b0vbJ9DYS0AdM"
request methodGet "/authors_only" [auth] "" request methodGet "/authors_only" [auth] ""
`shouldRespondWith` [json| {"message":"JWT expired"} |] `shouldRespondWith` [json| {"message":"JWT expired"} |]
{ matchStatus = 401 { matchStatus = 401
@@ -89,27 +89,27 @@ spec = describe "authorization" $ do
it "hides tables from users with invalid JWT" $ do it "hides tables from users with invalid JWT" $ do
let auth = authHeaderJWT "ey9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0" let auth = authHeaderJWT "ey9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
request methodGet "/authors_only" [auth] "" request methodGet "/authors_only" [auth] ""
`shouldRespondWith` [json| {"message":"JWT invalid"} |] `shouldRespondWith` [json| {"message":"JWSError (CompactDecodeError \"expected 3 parts, got 2\")"} |]
{ matchStatus = 401 { matchStatus = 401
, matchHeaders = [ , matchHeaders = [
"WWW-Authenticate" <:> "WWW-Authenticate" <:>
"Bearer error=\"invalid_token\", error_description=\"JWT invalid\"" "Bearer error=\"invalid_token\", error_description=\"JWSError (CompactDecodeError \\\"expected 3 parts, got 2\\\")\""
] ]
} }
it "should fail when jwt contains no claims" $ do it "should fail when jwt contains no claims" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.lu-rG8aSCiw-aOlN0IxpRGz5r7Jwq7K9r3tuMPUpytI" let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.CUIP5V9thWsGGFsFyGijSZf1fJMfarLHI9CEJL-TGNk"
request methodGet "/authors_only" [auth] "" request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 401 `shouldRespondWith` 401
it "hides tables from users with JWT that contain no claims about role" $ do it "hides tables from users with JWT that contain no claims about role" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Impkb2UifQ.Jneso9X519Vh0z7i9PbXIu7W1HEoq9RRw9BBbyQKFCQ" let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Impkb2UifQ.RVlZDaSyKbFPvxUf3V_NQXybfRB4dlBIkAUQXVXLUAI"
request methodGet "/authors_only" [auth] "" request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 401 `shouldRespondWith` 401
it "recovers after 401 error with logged in user" $ do it "recovers after 401 error with logged in user" $ do
_ <- post "/authors_only" [json| { "owner": "jdoe", "secret": "test content" } |] _ <- post "/authors_only" [json| { "owner": "jdoe", "secret": "test content" } |]
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0" let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0"
_ <- request methodPost "/rpc/problem" [auth] "" _ <- request methodPost "/rpc/problem" [auth] ""
request methodGet "/authors_only" [auth] "" request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200 `shouldRespondWith` 200
@@ -117,7 +117,7 @@ spec = describe "authorization" $ do
describe "custom pre-request proc acting on id claim" $ do describe "custom pre-request proc acting on id claim" $ do
it "able to switch to postgrest_test_author role (id=1)" $ it "able to switch to postgrest_test_author role (id=1)" $
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MX0.mI2HNoOum6xM3sc4oHLxU4yLv-_WV5W1kqBfY_wEvLw" in let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MX0.gKw7qI50i9hMrSJW8BlTpdMEVmMXJYxlAqueGqpa_mE" in
request methodPost "/rpc/get_current_user" [auth] request methodPost "/rpc/get_current_user" [auth]
[json| {} |] [json| {} |]
`shouldRespondWith` [str|"postgrest_test_author"|] `shouldRespondWith` [str|"postgrest_test_author"|]
@@ -126,7 +126,7 @@ spec = describe "authorization" $ do
} }
it "able to switch to postgrest_test_default_role (id=2)" $ it "able to switch to postgrest_test_default_role (id=2)" $
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Mn0.W7jLsG-zswM91AJkCvZeIMHrnz7_6ceY2jnscVl3Yhk" in let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Mn0.nwzjMI0YLvVGJQTeoCPEBsK983b__gxdpLXisBNaO2A" in
request methodPost "/rpc/get_current_user" [auth] request methodPost "/rpc/get_current_user" [auth]
[json| {} |] [json| {} |]
`shouldRespondWith` [str|"postgrest_test_default_role"|] `shouldRespondWith` [str|"postgrest_test_default_role"|]
@@ -135,7 +135,7 @@ spec = describe "authorization" $ do
} }
it "raises error (id=3)" $ it "raises error (id=3)" $
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6M30.15Gy8PezQhJIaHYDJVLa-Gmz9T3sJnW66EKAYIsXc7c" in let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6M30.OGxEJAf60NKZiTn-tIb2jy4rqKs_ZruLGWZ40TjrJsM" in
request methodPost "/rpc/get_current_user" [auth] request methodPost "/rpc/get_current_user" [auth]
[json| {} |] [json| {} |]
`shouldRespondWith` [str|{"hint":"Please contact administrator","details":null,"code":"P0001","message":"Disabled ID --> 3"}|] `shouldRespondWith` [str|{"hint":"Please contact administrator","details":null,"code":"P0001","message":"Disabled ID --> 3"}|]
+1 -1
View File
@@ -16,6 +16,6 @@ spec = describe "server started with binary JWT secret" $
-- this test will stop working 9999999999s after the UNIX EPOCH -- this test will stop working 9999999999s after the UNIX EPOCH
it "succeeds with jwt token encoded with a binary secret" $ do it "succeeds with jwt token encoded with a binary secret" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.l_EcSRWeNtL4OKUTIplrHyioNrff9Rd0MV7RXNCxCyk" let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.Dpss-QoLYjec5OTsOaAc3FNVsSjA89wACoV-0ra3ClA"
request methodGet "/authors_only" [auth] "" request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200 `shouldRespondWith` 200
+2 -2
View File
@@ -451,7 +451,7 @@ spec = do
describe "Row level permission" $ describe "Row level permission" $
it "set user_id when inserting rows" $ do it "set user_id when inserting rows" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0" let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0"
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |] _ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
_ <- post "/postgrest/users" [json| { "id":"jroe", "pass": "1234", "role": "postgrest_test_author" } |] _ <- post "/postgrest/users" [json| { "id":"jroe", "pass": "1234", "role": "postgrest_test_author" } |]
@@ -464,7 +464,7 @@ spec = do
p2 <- request methodPost "/authors_only" p2 <- request methodPost "/authors_only"
-- jwt token for jroe -- jwt token for jroe
[ authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqcm9lIn0.YuF_VfmyIxWyuceT7crnNKEprIYXsJAyXid3rjPjIow", ("Prefer", "return=representation") ] [ authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqcm9lIn0.2e7mx0U4uDcInlbJVOBGlrRufwqWLINDIEDC1vS0nw8", ("Prefer", "return=representation") ]
[json| { "secret": "lolcat", "owner": "hacker" } |] [json| { "secret": "lolcat", "owner": "hacker" } |]
liftIO $ do liftIO $ do
simpleBody p2 `shouldBe` [str|[{"owner":"jroe","secret":"lolcat"}]|] simpleBody p2 `shouldBe` [str|[{"owner":"jroe","secret":"lolcat"}]|]
+1 -1
View File
@@ -16,7 +16,7 @@ spec = describe "server started without JWT secret" $ do
-- this test will stop working 9999999999s after the UNIX EPOCH -- this test will stop working 9999999999s after the UNIX EPOCH
it "responds with error on attempted auth" $ do it "responds with error on attempted auth" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.QaPPLWTuyydMu_q7H4noMT7Lk6P4muet1OpJXF6ofhc" let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.Dpss-QoLYjec5OTsOaAc3FNVsSjA89wACoV-0ra3ClA"
request methodGet "/authors_only" [auth] "" request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 500 `shouldRespondWith` 500
+12 -12
View File
@@ -7,12 +7,11 @@ import qualified Hasql.Pool as P
import PostgREST.DbStructure (getDbStructure) import PostgREST.DbStructure (getDbStructure)
import PostgREST.App (postgrest) import PostgREST.App (postgrest)
import Control.AutoUpdate
import Data.Function (id) import Data.Function (id)
import Data.IORef import Data.IORef
import Data.Time.Clock.POSIX (getPOSIXTime)
import qualified Feature.AuthSpec import qualified Feature.AuthSpec
import qualified Feature.AsymmetricJwtSpec
import qualified Feature.BinaryJwtSecretSpec import qualified Feature.BinaryJwtSecretSpec
import qualified Feature.ConcurrentSpec import qualified Feature.ConcurrentSpec
import qualified Feature.CorsSpec import qualified Feature.CorsSpec
@@ -36,19 +35,16 @@ main = do
setupDb testDbConn setupDb testDbConn
pool <- P.acquire (3, 10, toS testDbConn) pool <- P.acquire (3, 10, toS testDbConn)
-- ask for the OS time at most once per second
getTime <- mkAutoUpdate
defaultUpdateSettings { updateAction = getPOSIXTime }
result <- P.use pool $ getDbStructure "test" result <- P.use pool $ getDbStructure "test"
refDbStructure <- newIORef $ Just $ either (panic.show) id result refDbStructure <- newIORef $ Just $ either (panic.show) id result
let withApp = return $ postgrest (testCfg testDbConn) refDbStructure pool getTime $ pure () let withApp = return $ postgrest (testCfg testDbConn) refDbStructure pool $ pure ()
ltdApp = return $ postgrest (testLtdRowsCfg testDbConn) refDbStructure pool getTime $ pure () ltdApp = return $ postgrest (testLtdRowsCfg testDbConn) refDbStructure pool $ pure ()
unicodeApp = return $ postgrest (testUnicodeCfg testDbConn) refDbStructure pool getTime $ pure () unicodeApp = return $ postgrest (testUnicodeCfg testDbConn) refDbStructure pool $ pure ()
proxyApp = return $ postgrest (testProxyCfg testDbConn) refDbStructure pool getTime $ pure () proxyApp = return $ postgrest (testProxyCfg testDbConn) refDbStructure pool $ pure ()
noJwtApp = return $ postgrest (testCfgNoJWT testDbConn) refDbStructure pool getTime $ pure () noJwtApp = return $ postgrest (testCfgNoJWT testDbConn) refDbStructure pool $ pure ()
binaryJwtApp = return $ postgrest (testCfgBinaryJWT testDbConn) refDbStructure pool getTime $ pure () binaryJwtApp = return $ postgrest (testCfgBinaryJWT testDbConn) refDbStructure pool $ pure ()
asymJwkApp = return $ postgrest (testCfgAsymJWK testDbConn) refDbStructure pool $ pure ()
let reset = resetDb testDbConn let reset = resetDb testDbConn
hspec $ do hspec $ do
@@ -74,6 +70,10 @@ main = do
beforeAll_ reset . before binaryJwtApp $ beforeAll_ reset . before binaryJwtApp $
describe "Feature.BinaryJwtSecretSpec" Feature.BinaryJwtSecretSpec.spec describe "Feature.BinaryJwtSecretSpec" Feature.BinaryJwtSecretSpec.spec
-- this test runs with asymmetric JWK
beforeAll_ reset . before asymJwkApp $
describe "Feature.AsymmetricJwtSpec" Feature.AsymmetricJwtSpec.spec
where where
specs = map (uncurry describe) [ specs = map (uncurry describe) [
("Feature.AuthSpec" , Feature.AuthSpec.spec) ("Feature.AuthSpec" , Feature.AuthSpec.spec)
+12 -4
View File
@@ -13,7 +13,8 @@ import Data.List (lookup)
import Text.Regex.TDFA ((=~)) import Text.Regex.TDFA ((=~))
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy as BL
import System.Process (readProcess) import System.Process (readProcess)
import Text.Heredoc
import PostgREST.Config (AppConfig(..)) import PostgREST.Config (AppConfig(..))
@@ -67,7 +68,7 @@ _baseCfg :: AppConfig
_baseCfg = -- Connection Settings _baseCfg = -- Connection Settings
AppConfig mempty "postgrest_test_anonymous" Nothing "test" "localhost" 3000 AppConfig mempty "postgrest_test_anonymous" Nothing "test" "localhost" 3000
-- Jwt settings -- Jwt settings
(Just $ encodeUtf8 "safe") False (Just $ encodeUtf8 "reallyreallyreallyreallyverysafe") False
-- Connection Modifiers -- Connection Modifiers
10 Nothing (Just "test.switch_role") 10 Nothing (Just "test.switch_role")
-- Debug Settings -- Debug Settings
@@ -89,9 +90,16 @@ testProxyCfg :: Text -> AppConfig
testProxyCfg testDbConn = (testCfg testDbConn) { configProxyUri = Just "https://postgrest.com/openapi.json" } testProxyCfg testDbConn = (testCfg testDbConn) { configProxyUri = Just "https://postgrest.com/openapi.json" }
testCfgBinaryJWT :: Text -> AppConfig testCfgBinaryJWT :: Text -> AppConfig
testCfgBinaryJWT testDbConn = (testCfg testDbConn) { configJwtSecret = Just secretBs } testCfgBinaryJWT testDbConn = (testCfg testDbConn) {
where secretBs = B64.decodeLenient "h2CGB1FoBd51aQooCS2g+UmRgYQfTPQ6v3+9ALbaqM4=" configJwtSecret = Just . B64.decodeLenient $
"cmVhbGx5cmVhbGx5cmVhbGx5cmVhbGx5dmVyeXNhZmU="
}
testCfgAsymJWK :: Text -> AppConfig
testCfgAsymJWK testDbConn = (testCfg testDbConn) {
configJwtSecret = Just $ encodeUtf8
[str|{"alg":"RS256","e":"AQAB","key_ops":["verify"],"kty":"RSA","n":"0etQ2Tg187jb04MWfpuogYGV75IFrQQBxQaGH75eq_FpbkyoLcEpRUEWSbECP2eeFya2yZ9vIO5ScD-lPmovePk4Aa4SzZ8jdjhmAbNykleRPCxMg0481kz6PQhnHRUv3nF5WP479CnObJKqTVdEagVL66oxnX9VhZG9IZA7k0Th5PfKQwrKGyUeTGczpOjaPqbxlunP73j9AfnAt4XCS8epa-n3WGz1j-wfpr_ys57Aq-zBCfqP67UYzNpeI1AoXsJhD9xSDOzvJgFRvc3vm2wjAW4LEMwi48rCplamOpZToIHEPIaPzpveYQwDnB1HFTR1ove9bpKJsHmi-e2uzQ","use":"sig"}|]
}
setupDb :: Text -> IO () setupDb :: Text -> IO ()
setupDb dbConn = do setupDb dbConn = do
+3 -4
View File
@@ -205,7 +205,7 @@ CREATE FUNCTION login(id text, pass text) RETURNS public.jwt_token
LANGUAGE sql SECURITY DEFINER LANGUAGE sql SECURITY DEFINER
AS $$ AS $$
SELECT jwt.sign( SELECT jwt.sign(
row_to_json(r), 'safe' row_to_json(r), 'reallyreallyreallyreallyverysafe'
) as token ) as token
FROM ( FROM (
SELECT rolname::text, id::text SELECT rolname::text, id::text
@@ -238,7 +238,7 @@ CREATE FUNCTION jwt_test() RETURNS public.jwt_token
LANGUAGE sql SECURITY DEFINER LANGUAGE sql SECURITY DEFINER
AS $$ AS $$
SELECT jwt.sign( SELECT jwt.sign(
row_to_json(r), 'safe' row_to_json(r), 'reallyreallyreallyreallyverysafe'
) as token ) as token
FROM ( FROM (
SELECT 'joe'::text as iss, 'fun'::text as sub, 'everyone'::text as aud, SELECT 'joe'::text as iss, 'fun'::text as sub, 'everyone'::text as aud,
@@ -279,14 +279,13 @@ $$;
-- --
CREATE FUNCTION reveal_big_jwt() RETURNS TABLE ( CREATE FUNCTION reveal_big_jwt() RETURNS TABLE (
iss text, sub text, aud text, exp bigint, iss text, sub text, exp bigint,
nbf bigint, iat bigint, jti text, "http://postgrest.com/foo" boolean nbf bigint, iat bigint, jti text, "http://postgrest.com/foo" boolean
) )
LANGUAGE sql SECURITY DEFINER LANGUAGE sql SECURITY DEFINER
AS $$ AS $$
SELECT current_setting('request.jwt.claim.iss') as iss, SELECT current_setting('request.jwt.claim.iss') as iss,
current_setting('request.jwt.claim.sub') as sub, current_setting('request.jwt.claim.sub') as sub,
current_setting('request.jwt.claim.aud') as aud,
current_setting('request.jwt.claim.exp')::bigint as exp, current_setting('request.jwt.claim.exp')::bigint as exp,
current_setting('request.jwt.claim.nbf')::bigint as nbf, current_setting('request.jwt.claim.nbf')::bigint as nbf,
current_setting('request.jwt.claim.iat')::bigint as iat, current_setting('request.jwt.claim.iat')::bigint as iat,
+1 -1
View File
@@ -1 +1 @@
QSBCIEMKSXQncyBlYXN5IGFzLCAxIDIgMw== QUJDCkVhc3kgYXMKMTIzCk9yIHNpbXBsZSBhcwpEbyByZSBtaQ==
+1 -1
View File
@@ -1 +1 @@
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.MsR2A5HkhQdBsuQhXH8TvUdlvezBm5JEu4SOmHj34KI eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.p__cvqgrtteKUhxrJwTwqxeuFmlEm3hEj1mk7yO15M4
+5 -2
View File
@@ -1,2 +1,5 @@
A B C ABC
It's easy as, 1 2 3 Easy as
123
Or simple as
Do re mi
+5 -2
View File
@@ -1,2 +1,5 @@
A B C ABC
It's easy as, 1 2 3 Easy as
123
Or simple as
Do re mi
+1 -1
View File
@@ -1 +1 @@
i6aWVaZ4Zt8= RTwSHLM0/PWM2YCOyBiyChMQQamZLTZGrXdzGk61o5A=
+2 -1
View File
@@ -1 +1,2 @@
協剖ヲxf゚ E<³4üõŒÙ€ŽÈ²
A©™-6F­wsNµ£
+1 -1
View File
@@ -1 +1 @@
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.ySt_r_BN596NjZGcUAnlMAflDARDjrsR2c-fkOWlbFs eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.zyXYJKXgVYaWqMYgUXMk4zqXSU5cwA_vK2z9_5lRlqA
+2 -1
View File
@@ -1 +1,2 @@
協剖ヲxf゚ E<³4üõŒÙ€ŽÈ²
A©™-6F­wsNµ£
+1 -1
View File
@@ -1 +1 @@
QUJDMTIz QUJDRWFzeUFzT25lVHdvVGhyZWVPclNpbXBsZUFzRG9SZU1p
+1 -1
View File
@@ -1 +1 @@
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.yERzinJhzJ5XYKZuxVroqsAwGXMtCxntfm8HVxc1amI eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.1oyYyosD3_z4YzBE1DB-Pxf7wC0SCuZ2u1zLzPYU_nQ
+1 -1
View File
@@ -1 +1 @@
ABC123 ABCEasyAsOneTwoThreeOrSimpleAsDoReMi
+1 -1
View File
@@ -1 +1 @@
ABC123 ABCEasyAsOneTwoThreeOrSimpleAsDoReMi