Merge pull request #703 from begriffs/jwt
Big bag of JWT fixes for accumulated issues
This commit is contained in:
+5
-1
@@ -7,12 +7,13 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Ability to generate an OpenAPI spec - @mainx07, @hudayou, @ruslantalpa, @begriffs
|
- Ability to generate an OpenAPI spec - @mainx07, @hudayou, @ruslantalpa, @begriffs
|
||||||
- Ability to generate an OpenAPI spec behind a proxy- @hudayou
|
- Ability to generate an OpenAPI spec behind a proxy - @hudayou
|
||||||
- Ability to set addresses to listen on - @hudayou
|
- Ability to set addresses to listen on - @hudayou
|
||||||
- Filtering, shaping and embedding with &select for the /rpc path - @ruslantalpa
|
- Filtering, shaping and embedding with &select for the /rpc path - @ruslantalpa
|
||||||
- Output names of used-defined types (instead of 'USER-DEFINED') - @martingms
|
- Output names of used-defined types (instead of 'USER-DEFINED') - @martingms
|
||||||
- Implement support for singular representation responses for POST/PATCH requests - @ehamberg
|
- Implement support for singular representation responses for POST/PATCH requests - @ehamberg
|
||||||
- Include RPC endpoints in OpenAPI output - @begriffs, @LogvinovLeon
|
- Include RPC endpoints in OpenAPI output - @begriffs, @LogvinovLeon
|
||||||
|
- Custom request validation with `--pre-request` argument - @begriffs
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Do not apply limit to parent items - @ruslantalpa
|
- Do not apply limit to parent items - @ruslantalpa
|
||||||
@@ -23,6 +24,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- Remove non-OpenAPI schema description - @begriffs
|
- Remove non-OpenAPI schema description - @begriffs
|
||||||
- Use comma rather than semicolon to separate Prefer header values - @begriffs
|
- Use comma rather than semicolon to separate Prefer header values - @begriffs
|
||||||
- Omit total query count by default - @begriffs
|
- Omit total query count by default - @begriffs
|
||||||
|
- No more reserved `jwt_claims` return type - @begriffs
|
||||||
|
- HTTP 401 rather than 400 for expired JWT - @begriffs
|
||||||
|
- Remove default JWT secret - @begriffs
|
||||||
|
|
||||||
## [0.3.2.0] - 2016-06-10
|
## [0.3.2.0] - 2016-06-10
|
||||||
|
|
||||||
|
|||||||
+17
-5
@@ -11,8 +11,11 @@ import PostgREST.Config (AppConfig (..),
|
|||||||
import PostgREST.OpenAPI (isMalformedProxyUri)
|
import PostgREST.OpenAPI (isMalformedProxyUri)
|
||||||
import PostgREST.DbStructure
|
import PostgREST.DbStructure
|
||||||
|
|
||||||
|
import Control.AutoUpdate
|
||||||
import Data.String (IsString (..))
|
import Data.String (IsString (..))
|
||||||
|
import Data.Text (stripPrefix)
|
||||||
import Data.Function (id)
|
import Data.Function (id)
|
||||||
|
import Data.Time.Clock.POSIX (getPOSIXTime)
|
||||||
import qualified Hasql.Query as H
|
import qualified Hasql.Query as H
|
||||||
import qualified Hasql.Session as H
|
import qualified Hasql.Session as H
|
||||||
import qualified Hasql.Decoders as HD
|
import qualified Hasql.Decoders as HD
|
||||||
@@ -21,7 +24,6 @@ import qualified Hasql.Pool as P
|
|||||||
import Network.Wai.Handler.Warp
|
import Network.Wai.Handler.Warp
|
||||||
import System.IO (BufferMode (..),
|
import System.IO (BufferMode (..),
|
||||||
hSetBuffering)
|
hSetBuffering)
|
||||||
import Web.JWT (secret)
|
|
||||||
import Data.IORef
|
import Data.IORef
|
||||||
#ifndef mingw32_HOST_OS
|
#ifndef mingw32_HOST_OS
|
||||||
import System.Posix.Signals
|
import System.Posix.Signals
|
||||||
@@ -42,7 +44,7 @@ main = do
|
|||||||
hSetBuffering stdin LineBuffering
|
hSetBuffering stdin LineBuffering
|
||||||
hSetBuffering stderr NoBuffering
|
hSetBuffering stderr NoBuffering
|
||||||
|
|
||||||
conf <- readOptions
|
conf <- loadSecretFile =<< readOptions
|
||||||
let host = configHost conf
|
let host = configHost conf
|
||||||
port = configPort conf
|
port = configPort conf
|
||||||
proxy = configProxyUri conf
|
proxy = configProxyUri conf
|
||||||
@@ -55,8 +57,6 @@ main = do
|
|||||||
when (isMalformedProxyUri $ toS <$> proxy) $ panic
|
when (isMalformedProxyUri $ toS <$> proxy) $ panic
|
||||||
"Malformed proxy uri, a correct example: https://example.com:8443/basePath"
|
"Malformed proxy uri, a correct example: https://example.com:8443/basePath"
|
||||||
|
|
||||||
unless (secret "secret" /= configJwtSecret conf) $
|
|
||||||
putStrLn ("WARNING, running in insecure mode, JWT secret is the default value" :: Text)
|
|
||||||
putStrLn $ ("Listening on port " :: Text) <> show (configPort conf)
|
putStrLn $ ("Listening on port " :: Text) <> show (configPort conf)
|
||||||
|
|
||||||
pool <- P.acquire (configPool conf, 10, pgSettings)
|
pool <- P.acquire (configPool conf, 10, pgSettings)
|
||||||
@@ -85,4 +85,16 @@ main = do
|
|||||||
) Nothing
|
) Nothing
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
runSettings appSettings $ postgrest conf refDbStructure pool
|
-- ask for the OS time at most once per second
|
||||||
|
getTime <- mkAutoUpdate
|
||||||
|
defaultUpdateSettings { updateAction = getPOSIXTime }
|
||||||
|
|
||||||
|
runSettings appSettings $ postgrest conf refDbStructure pool getTime
|
||||||
|
|
||||||
|
loadSecretFile :: AppConfig -> IO AppConfig
|
||||||
|
loadSecretFile conf = do
|
||||||
|
let s = configJwtSecret conf
|
||||||
|
real <- case join (stripPrefix "@" <$> s) of
|
||||||
|
Nothing -> return s -- the string is the secret, not a filename
|
||||||
|
Just filename -> sequence . Just $ readFile (toS filename)
|
||||||
|
return conf { configJwtSecret = real }
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ executable postgrest
|
|||||||
"-with-rtsopts=-N -I2"
|
"-with-rtsopts=-N -I2"
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
build-depends: aeson (>= 0.8 && < 0.10) || (>= 0.11 && < 0.12)
|
build-depends: aeson (>= 0.8 && < 0.10) || (>= 0.11 && < 0.12)
|
||||||
|
, auto-update
|
||||||
, base >= 4.8 && < 6
|
, base >= 4.8 && < 6
|
||||||
, bytestring
|
, bytestring
|
||||||
, bytestring-tree-builder == 0.2.7
|
, bytestring-tree-builder == 0.2.7
|
||||||
@@ -143,6 +144,7 @@ Test-Suite spec
|
|||||||
, Feature.CorsSpec
|
, Feature.CorsSpec
|
||||||
, Feature.DeleteSpec
|
, Feature.DeleteSpec
|
||||||
, Feature.InsertSpec
|
, Feature.InsertSpec
|
||||||
|
, Feature.NoJwtSpec
|
||||||
, Feature.QuerySpec
|
, Feature.QuerySpec
|
||||||
, Feature.QueryLimitedSpec
|
, Feature.QueryLimitedSpec
|
||||||
, Feature.RangeSpec
|
, Feature.RangeSpec
|
||||||
@@ -151,6 +153,7 @@ Test-Suite spec
|
|||||||
, SpecHelper
|
, SpecHelper
|
||||||
, TestTypes
|
, TestTypes
|
||||||
Build-Depends: aeson
|
Build-Depends: aeson
|
||||||
|
, auto-update
|
||||||
, aeson-qq
|
, aeson-qq
|
||||||
, async
|
, async
|
||||||
, base
|
, base
|
||||||
|
|||||||
+15
-19
@@ -7,12 +7,13 @@ module PostgREST.App (
|
|||||||
) where
|
) where
|
||||||
|
|
||||||
import Control.Applicative
|
import Control.Applicative
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import Data.IORef (IORef, readIORef)
|
import Data.IORef (IORef, readIORef)
|
||||||
import Data.List (delete, lookup)
|
import Data.List (delete, lookup)
|
||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
import Data.Ranged.Ranges (emptyRange)
|
import Data.Ranged.Ranges (emptyRange)
|
||||||
import Data.Text (replace, strip, isInfixOf, dropWhile, drop, intercalate)
|
import Data.Text (replace, strip, isInfixOf, dropWhile, drop, intercalate)
|
||||||
|
import Data.Time.Clock.POSIX (POSIXTime)
|
||||||
import Data.Tree
|
import Data.Tree
|
||||||
|
|
||||||
import qualified Hasql.Pool as P
|
import qualified Hasql.Pool as P
|
||||||
@@ -25,13 +26,13 @@ import qualified Text.InterpolatedString.Perl6 as P6 (q)
|
|||||||
|
|
||||||
import Network.HTTP.Types.Header
|
import Network.HTTP.Types.Header
|
||||||
import Network.HTTP.Types.Status
|
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 (secret)
|
||||||
|
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
import Data.Aeson.Types (emptyArray)
|
import Data.Aeson.Types (emptyArray)
|
||||||
import Data.Time.Clock.POSIX (getPOSIXTime)
|
|
||||||
import qualified Data.Vector as V
|
import qualified Data.Vector as V
|
||||||
import qualified Hasql.Transaction as H
|
import qualified Hasql.Transaction as H
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ import PostgREST.ApiRequest (ApiRequest(..), ContentType(..)
|
|||||||
, ctToHeader
|
, ctToHeader
|
||||||
, userApiRequest
|
, userApiRequest
|
||||||
, toHeader)
|
, toHeader)
|
||||||
import PostgREST.Auth (tokenJWT, jwtClaims, containsRole)
|
import PostgREST.Auth (jwtClaims, containsRole)
|
||||||
import PostgREST.Config (AppConfig (..))
|
import PostgREST.Config (AppConfig (..))
|
||||||
import PostgREST.DbStructure
|
import PostgREST.DbStructure
|
||||||
import PostgREST.Error (errResponse, pgErrResponse)
|
import PostgREST.Error (errResponse, pgErrResponse)
|
||||||
@@ -68,18 +69,20 @@ import Data.Foldable (foldr1)
|
|||||||
import Data.Function (id)
|
import Data.Function (id)
|
||||||
import Protolude hiding (dropWhile, drop, intercalate, Proxy)
|
import Protolude hiding (dropWhile, drop, intercalate, Proxy)
|
||||||
|
|
||||||
postgrest :: AppConfig -> IORef DbStructure -> P.Pool -> Application
|
postgrest :: AppConfig -> IORef DbStructure -> P.Pool -> IO POSIXTime ->
|
||||||
postgrest conf refDbStructure pool =
|
Application
|
||||||
|
postgrest conf refDbStructure pool getTime =
|
||||||
let middle = (if configQuiet conf then id else logStdout) . defaultMiddle in
|
let middle = (if configQuiet conf then id else logStdout) . defaultMiddle in
|
||||||
|
|
||||||
middle $ \ req respond -> do
|
middle $ \ req respond -> do
|
||||||
time <- getPOSIXTime
|
time <- getTime
|
||||||
body <- strictRequestBody req
|
body <- strictRequestBody req
|
||||||
dbStructure <- readIORef refDbStructure
|
dbStructure <- readIORef refDbStructure
|
||||||
|
|
||||||
let schema = toS $ configSchema conf
|
let schema = toS $ configSchema conf
|
||||||
apiRequest = userApiRequest schema req body
|
apiRequest = userApiRequest schema req body
|
||||||
eClaims = jwtClaims (configJwtSecret conf) (iJWT apiRequest) time
|
eClaims = jwtClaims
|
||||||
|
(secret <$> configJwtSecret conf) (iJWT apiRequest) time
|
||||||
authed = containsRole eClaims
|
authed = containsRole eClaims
|
||||||
handleReq = runWithClaims conf eClaims (app dbStructure conf) apiRequest
|
handleReq = runWithClaims conf eClaims (app dbStructure conf) apiRequest
|
||||||
txMode = transactionMode $ iAction apiRequest
|
txMode = transactionMode $ iAction apiRequest
|
||||||
@@ -211,21 +214,14 @@ app dbStructure conf apiRequest =
|
|||||||
Just (PayloadJSON (UniformObjects payload))) -> do
|
Just (PayloadJSON (UniformObjects payload))) -> do
|
||||||
let p = V.head payload
|
let p = V.head payload
|
||||||
singular = iPreferSingular apiRequest
|
singular = iPreferSingular apiRequest
|
||||||
jwtSecret = configJwtSecret conf
|
|
||||||
returnType = lookup (qiName qi) $ dbProcs dbStructure
|
|
||||||
returnsJWT = fromMaybe False $
|
|
||||||
isInfixOf "jwt_claims" . pdReturnType <$> returnType
|
|
||||||
serves [CTApplicationJSON] (iAccepts apiRequest) $ \_ -> case readSqlParts of
|
serves [CTApplicationJSON] (iAccepts apiRequest) $ \_ -> case readSqlParts of
|
||||||
Left e -> return $ responseLBS status400 [jsonH] $ toS e
|
Left e -> return $ responseLBS status400 [jsonH] $ toS e
|
||||||
Right (q,cq) -> respondToRange $ do
|
Right (q,cq) -> respondToRange $ do
|
||||||
row <- H.query () (callProc qi p q cq topLevelRange shouldCount singular)
|
row <- H.query () (callProc qi p q cq topLevelRange shouldCount singular)
|
||||||
let (tableTotal, queryTotal, body) = fromMaybe (Just 0, 0, emptyArray) row
|
let (tableTotal, queryTotal, body) =
|
||||||
|
fromMaybe (Just 0, 0, emptyArray) row
|
||||||
(status, contentRange) = rangeHeader queryTotal tableTotal
|
(status, contentRange) = rangeHeader queryTotal tableTotal
|
||||||
in
|
return $ responseLBS status [jsonH, contentRange] (toS . encode $ body)
|
||||||
return $ responseLBS status [jsonH, contentRange]
|
|
||||||
(if returnsJWT
|
|
||||||
then "{\"token\":\"" <> toS (tokenJWT jwtSecret body) <> "\"}"
|
|
||||||
else toS $ encode body)
|
|
||||||
|
|
||||||
(ActionRead, TargetRoot, Nothing) -> do
|
(ActionRead, TargetRoot, Nothing) -> do
|
||||||
let host = configHost conf
|
let host = configHost conf
|
||||||
|
|||||||
+25
-13
@@ -16,6 +16,7 @@ module PostgREST.Auth (
|
|||||||
, containsRole
|
, containsRole
|
||||||
, jwtClaims
|
, jwtClaims
|
||||||
, tokenJWT
|
, tokenJWT
|
||||||
|
, JWTAttempt(..)
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
@@ -47,22 +48,33 @@ claimsToSQL claims = roleStmts <> varStmts
|
|||||||
valueToVariable = pgFmtLit . unquoted
|
valueToVariable = pgFmtLit . unquoted
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Receives the JWT secret (from config) and a JWT and
|
Possible situations encountered with client JWTs
|
||||||
returns a map of JWT claims
|
|
||||||
In case there is any problem decoding the JWT it returns an error Text
|
|
||||||
-}
|
-}
|
||||||
jwtClaims :: JWT.Secret -> Text -> NominalDiffTime -> Either Text (M.HashMap Text Value)
|
data JWTAttempt = JWTExpired
|
||||||
jwtClaims _ "" _ = Right M.empty
|
| JWTInvalid
|
||||||
|
| JWTMissingSecret
|
||||||
|
| JWTClaims (M.HashMap Text Value)
|
||||||
|
deriving Eq
|
||||||
|
|
||||||
|
{-|
|
||||||
|
Receives the JWT secret (from config) and a JWT and returns a map
|
||||||
|
of JWT claims.
|
||||||
|
-}
|
||||||
|
jwtClaims :: Maybe JWT.Secret -> Text -> NominalDiffTime -> JWTAttempt
|
||||||
|
jwtClaims _ "" _ = JWTClaims M.empty
|
||||||
jwtClaims secret jwt time =
|
jwtClaims secret jwt time =
|
||||||
case isExpired <$> mClaims of
|
case secret of
|
||||||
Just True -> Left "JWT expired"
|
Nothing -> JWTMissingSecret
|
||||||
Nothing -> Left "Invalid JWT"
|
Just s ->
|
||||||
Just False -> Right $ value2map $ fromJust mClaims
|
let mClaims = toJSON . JWT.claims <$> JWT.decodeAndVerifySignature s jwt in
|
||||||
|
case isExpired <$> mClaims of
|
||||||
|
Just True -> JWTExpired
|
||||||
|
Nothing -> JWTInvalid
|
||||||
|
Just False -> JWTClaims $ value2map $ fromJust mClaims
|
||||||
where
|
where
|
||||||
isExpired claims =
|
isExpired claims =
|
||||||
let mExp = claims ^? key "exp" . _Integer
|
let mExp = claims ^? key "exp" . _Integer
|
||||||
in fromMaybe False $ (<= time) . fromInteger <$> mExp
|
in fromMaybe False $ (<= time) . fromInteger <$> mExp
|
||||||
mClaims = toJSON . JWT.claims <$> JWT.decodeAndVerifySignature secret jwt
|
|
||||||
value2map (Object o) = o
|
value2map (Object o) = o
|
||||||
value2map _ = M.empty
|
value2map _ = M.empty
|
||||||
|
|
||||||
@@ -80,6 +92,6 @@ tokenJWT secret _ = tokenJWT secret emptyArray
|
|||||||
{-|
|
{-|
|
||||||
Whether a response from jwtClaims contains a role claim
|
Whether a response from jwtClaims contains a role claim
|
||||||
-}
|
-}
|
||||||
containsRole :: Either Text (M.HashMap Text Value) -> Bool
|
containsRole :: JWTAttempt -> Bool
|
||||||
containsRole (Left _) = False
|
containsRole (JWTClaims claims) = M.member "role" claims
|
||||||
containsRole (Right claims) = M.member "role" claims
|
containsRole _ = False
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import Options.Applicative
|
|||||||
import Paths_postgrest (version)
|
import Paths_postgrest (version)
|
||||||
import Protolude hiding (intercalate)
|
import Protolude hiding (intercalate)
|
||||||
import Safe (readMay)
|
import Safe (readMay)
|
||||||
import Web.JWT (Secret, secret)
|
|
||||||
|
|
||||||
-- | Data type to store all command line options
|
-- | Data type to store all command line options
|
||||||
data AppConfig = AppConfig {
|
data AppConfig = AppConfig {
|
||||||
@@ -41,9 +40,10 @@ data AppConfig = AppConfig {
|
|||||||
, configSchema :: Text
|
, configSchema :: Text
|
||||||
, configHost :: Text
|
, configHost :: Text
|
||||||
, configPort :: Int
|
, configPort :: Int
|
||||||
, configJwtSecret :: Secret
|
, configJwtSecret :: Maybe Text
|
||||||
, configPool :: Int
|
, configPool :: Int
|
||||||
, configMaxRows :: Maybe Integer
|
, configMaxRows :: Maybe Integer
|
||||||
|
, configReqCheck :: Maybe Text
|
||||||
, configQuiet :: Bool
|
, configQuiet :: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,10 +55,10 @@ argParser = AppConfig
|
|||||||
<*> (toS <$> strOption (long "schema" <> short 's' <> help "schema to use for API routes" <> metavar "NAME" <> value "public" <> showDefault))
|
<*> (toS <$> strOption (long "schema" <> short 's' <> help "schema to use for API routes" <> metavar "NAME" <> value "public" <> showDefault))
|
||||||
<*> (toS <$> strOption (long "host" <> short 'l' <> help "hostname or ip on which to run HTTP server" <> metavar "HOST" <> value "*4" <> showDefault))
|
<*> (toS <$> strOption (long "host" <> short 'l' <> help "hostname or ip on which to run HTTP server" <> metavar "HOST" <> value "*4" <> showDefault))
|
||||||
<*> option auto (long "port" <> short 'p' <> help "port number on which to run HTTP server" <> metavar "PORT" <> value 3000 <> showDefault)
|
<*> option auto (long "port" <> short 'p' <> help "port number on which to run HTTP server" <> metavar "PORT" <> value 3000 <> showDefault)
|
||||||
<*> (secret . toS <$>
|
<*> (optional . map toS <$> strOption) (long "jwt-secret" <> short 'j' <> help "secret used to encrypt and decrypt JWT tokens" <> metavar "SECRET")
|
||||||
strOption (long "jwt-secret" <> short 'j' <> help "secret used to encrypt and decrypt JWT tokens" <> metavar "SECRET" <> value "secret" <> showDefault))
|
|
||||||
<*> option auto (long "pool" <> short 'o' <> help "max connections in database pool" <> metavar "COUNT" <> value 10 <> showDefault)
|
<*> option auto (long "pool" <> short 'o' <> help "max connections in database pool" <> metavar "COUNT" <> value 10 <> showDefault)
|
||||||
<*> (readMay <$> strOption (long "max-rows" <> short 'm' <> help "max rows in response" <> metavar "COUNT" <> value "infinity" <> showDefault))
|
<*> (readMay <$> strOption (long "max-rows" <> short 'm' <> help "max rows in response" <> metavar "COUNT" <> value "infinity" <> showDefault))
|
||||||
|
<*> (optional . map toS . strOption) (long "pre-request" <> help "schema-qualified name of proc to call to validate requests" <> metavar "FUNCTION")
|
||||||
<*> pure False
|
<*> pure False
|
||||||
|
|
||||||
defaultCorsPolicy :: CorsResourcePolicy
|
defaultCorsPolicy :: CorsResourcePolicy
|
||||||
|
|||||||
+25
-12
@@ -7,33 +7,46 @@ 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 (status400)
|
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)
|
||||||
|
|
||||||
import PostgREST.ApiRequest (ApiRequest(..))
|
import PostgREST.ApiRequest (ApiRequest(..), ContentType(..),
|
||||||
import PostgREST.Auth (claimsToSQL)
|
ctToHeader)
|
||||||
|
import PostgREST.Auth (claimsToSQL, JWTAttempt(..))
|
||||||
import PostgREST.Config (AppConfig (..), corsPolicy)
|
import PostgREST.Config (AppConfig (..), corsPolicy)
|
||||||
import PostgREST.Error (errResponse)
|
import PostgREST.Error (errResponse)
|
||||||
import Data.Text
|
|
||||||
|
|
||||||
import Protolude hiding (concat, null)
|
import Protolude hiding (concat, null)
|
||||||
|
|
||||||
runWithClaims :: AppConfig -> Either Text (M.HashMap Text Value) ->
|
runWithClaims :: AppConfig -> JWTAttempt ->
|
||||||
(ApiRequest -> H.Transaction Response) ->
|
(ApiRequest -> H.Transaction Response) ->
|
||||||
ApiRequest -> H.Transaction Response
|
ApiRequest -> H.Transaction Response
|
||||||
runWithClaims conf eClaims app req =
|
runWithClaims conf eClaims app req =
|
||||||
case eClaims of
|
case eClaims of
|
||||||
Left e -> clientErr e
|
JWTExpired -> return $ unauthed "JWT expired"
|
||||||
Right claims -> do
|
JWTInvalid -> return $ unauthed "JWT invalid"
|
||||||
-- role claim defaults to anon if not specified in jwt
|
JWTMissingSecret -> return $ errResponse status500 "Server lacks JWT secret"
|
||||||
H.sql . mconcat . claimsToSQL $ M.union claims (M.singleton "role" anon)
|
JWTClaims claims -> do
|
||||||
app req
|
-- role claim defaults to anon if not specified in jwt
|
||||||
|
let setClaims = claimsToSQL (M.union claims (M.singleton "role" anon))
|
||||||
|
H.sql $ mconcat setClaims
|
||||||
|
mapM_ H.sql customReqCheck
|
||||||
|
app req
|
||||||
where
|
where
|
||||||
anon = String . toS $ configAnonRole conf
|
anon = String . toS $ configAnonRole conf
|
||||||
clientErr = return . errResponse status400
|
customReqCheck = (\f -> "select " <> toS f <> "();") <$> configReqCheck conf
|
||||||
|
unauthed message = responseLBS unauthorized401
|
||||||
|
[ ctToHeader CTApplicationJSON
|
||||||
|
, ( "WWW-Authenticate"
|
||||||
|
, "Bearer error=\"invalid_token\", " <>
|
||||||
|
"error_description=\"" <> message <> "\""
|
||||||
|
)
|
||||||
|
]
|
||||||
|
(toS $ "{\"message\":\""<>message<>"\"}")
|
||||||
|
|
||||||
defaultMiddle :: Application -> Application
|
defaultMiddle :: Application -> Application
|
||||||
defaultMiddle =
|
defaultMiddle =
|
||||||
|
|||||||
+60
-11
@@ -1,6 +1,7 @@
|
|||||||
module Feature.AuthSpec where
|
module Feature.AuthSpec where
|
||||||
|
|
||||||
-- {{{ Imports
|
-- {{{ Imports
|
||||||
|
import Text.Heredoc
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
@@ -12,6 +13,7 @@ import Network.Wai (Application)
|
|||||||
|
|
||||||
spec :: SpecWith Application
|
spec :: SpecWith Application
|
||||||
spec = describe "authorization" $ do
|
spec = describe "authorization" $ do
|
||||||
|
let single = ("Prefer","plurality=singular")
|
||||||
|
|
||||||
it "denies access to tables that anonymous does not own" $
|
it "denies access to tables that anonymous does not own" $
|
||||||
get "/authors_only" `shouldRespondWith` ResponseMatcher {
|
get "/authors_only" `shouldRespondWith` ResponseMatcher {
|
||||||
@@ -38,17 +40,18 @@ spec = describe "authorization" $ do
|
|||||||
}
|
}
|
||||||
|
|
||||||
it "returns jwt functions as jwt tokens" $
|
it "returns jwt functions as jwt tokens" $
|
||||||
post "/rpc/login" [json| { "id": "jdoe", "pass": "1234" } |]
|
request methodPost "/rpc/login" [single]
|
||||||
|
[json| { "id": "jdoe", "pass": "1234" } |]
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"} |]
|
matchBody = Just [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xuYW1lIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.P2G9EVSVI22MWxXWFuhEYd9BZerLS1WDlqzdqplM15s"} |]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
|
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "sql functions can encode custom and standard claims" $
|
it "sql functions can encode custom and standard claims" $
|
||||||
post "/rpc/jwt_test" "{}"
|
request methodPost "/rpc/jwt_test" [single] "{}"
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmdW4iLCJqdGkiOiJmb28iLCJuYmYiOjEzMDA4MTkzODAsImV4cCI6MTMwMDgxOTM4MCwiaHR0cDovL3Bvc3RncmVzdC5jb20vZm9vIjp0cnVlLCJpc3MiOiJqb2UiLCJyb2xlIjoicG9zdGdyZXN0X3Rlc3QiLCJpYXQiOjEzMDA4MTkzODAsImF1ZCI6ImV2ZXJ5b25lIn0._tQCF79-ZZGMlLktd3csM_bVaiMg7A8YvIb6K2hcu5w"} |]
|
matchBody = Just [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJqb2UiLCJzdWIiOiJmdW4iLCJhdWQiOiJldmVyeW9uZSIsImV4cCI6MTMwMDgxOTM4MCwibmJmIjoxMzAwODE5MzgwLCJpYXQiOjEzMDA4MTkzODAsImp0aSI6ImZvbyIsInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdCIsImh0dHA6Ly9wb3N0Z3Jlc3QuY29tL2ZvbyI6dHJ1ZX0.IHF16ZSU6XTbOnUWO8CCpUn2fJwt8P00rlYVyXQjpWc"} |]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
|
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
|
||||||
}
|
}
|
||||||
@@ -80,26 +83,72 @@ spec = describe "authorization" $ do
|
|||||||
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.enk_qZ_u6gZsXY4R8bREKB_HNExRpM0lIWSLktk9JJQ"
|
||||||
request methodGet "/authors_only" [auth] ""
|
request methodGet "/authors_only" [auth] ""
|
||||||
`shouldRespondWith` 400
|
`shouldRespondWith` ResponseMatcher {
|
||||||
|
matchBody = Nothing
|
||||||
|
, matchStatus = 401
|
||||||
|
, matchHeaders = [
|
||||||
|
"WWW-Authenticate" <:>
|
||||||
|
"Bearer error=\"invalid_token\", error_description=\"JWT expired\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
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` 400
|
`shouldRespondWith` ResponseMatcher {
|
||||||
|
matchBody = Nothing
|
||||||
|
, matchStatus = 401
|
||||||
|
, matchHeaders = [
|
||||||
|
"WWW-Authenticate" <:>
|
||||||
|
"Bearer error=\"invalid_token\", error_description=\"JWT invalid\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
it "should fail when jwt contains no claims" $ do
|
it "should fail when jwt contains no claims" $ do
|
||||||
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.MKYc_lOECtB0LJOiykilAdlHodB-I0_id2qHKq35dmc"
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.lu-rG8aSCiw-aOlN0IxpRGz5r7Jwq7K9r3tuMPUpytI"
|
||||||
request methodGet "/authors_only" [auth] ""
|
request methodGet "/authors_only" [auth] ""
|
||||||
`shouldRespondWith` 400
|
`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.zyohGMnrDy4_8eJTl6I2AUXO3MeCCiwR24aGWRkTE9o"
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Impkb2UifQ.Jneso9X519Vh0z7i9PbXIu7W1HEoq9RRw9BBbyQKFCQ"
|
||||||
request methodGet "/authors_only" [auth] ""
|
request methodGet "/authors_only" [auth] ""
|
||||||
`shouldRespondWith` 400
|
`shouldRespondWith` 401
|
||||||
|
|
||||||
it "recovers after 400 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.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
|
||||||
_ <- request methodPost "/rpc/problem" [auth] ""
|
_ <- request methodPost "/rpc/problem" [auth] ""
|
||||||
request methodGet "/authors_only" [auth] ""
|
request methodGet "/authors_only" [auth] ""
|
||||||
`shouldRespondWith` 200
|
`shouldRespondWith` 200
|
||||||
|
|
||||||
|
describe "custom pre-request proc acting on id claim" $ do
|
||||||
|
|
||||||
|
it "able to switch to postgrest_test_author role (id=1)" $
|
||||||
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MX0.mI2HNoOum6xM3sc4oHLxU4yLv-_WV5W1kqBfY_wEvLw" in
|
||||||
|
request methodPost "/rpc/get_current_user" [auth]
|
||||||
|
[json| {} |]
|
||||||
|
`shouldRespondWith` ResponseMatcher {
|
||||||
|
matchBody = Just [str|"postgrest_test_author"|]
|
||||||
|
, matchStatus = 200
|
||||||
|
, matchHeaders = []
|
||||||
|
}
|
||||||
|
|
||||||
|
it "able to switch to postgrest_test_default_role (id=2)" $
|
||||||
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Mn0.W7jLsG-zswM91AJkCvZeIMHrnz7_6ceY2jnscVl3Yhk" in
|
||||||
|
request methodPost "/rpc/get_current_user" [auth]
|
||||||
|
[json| {} |]
|
||||||
|
`shouldRespondWith` ResponseMatcher {
|
||||||
|
matchBody = Just [str|"postgrest_test_default_role"|]
|
||||||
|
, matchStatus = 200
|
||||||
|
, matchHeaders = []
|
||||||
|
}
|
||||||
|
|
||||||
|
it "raises error (id=3)" $
|
||||||
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6M30.15Gy8PezQhJIaHYDJVLa-Gmz9T3sJnW66EKAYIsXc7c" in
|
||||||
|
request methodPost "/rpc/get_current_user" [auth]
|
||||||
|
[json| {} |]
|
||||||
|
`shouldRespondWith` ResponseMatcher {
|
||||||
|
matchBody = Just [str|{"hint":"Please contact administrator","details":null,"code":"P0001","message":"Disabled ID --> 3"}|]
|
||||||
|
, matchStatus = 400
|
||||||
|
, matchHeaders = []
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
module Feature.NoJwtSpec where
|
||||||
|
|
||||||
|
-- {{{ Imports
|
||||||
|
import Test.Hspec
|
||||||
|
import Test.Hspec.Wai
|
||||||
|
import Network.HTTP.Types
|
||||||
|
|
||||||
|
import SpecHelper
|
||||||
|
import Network.Wai (Application)
|
||||||
|
-- }}}
|
||||||
|
|
||||||
|
spec :: SpecWith Application
|
||||||
|
spec = describe "server started without JWT secret" $ do
|
||||||
|
|
||||||
|
-- this test will stop working 9999999999s after the UNIX EPOCH
|
||||||
|
it "responds with error on attempted auth" $ do
|
||||||
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.QaPPLWTuyydMu_q7H4noMT7Lk6P4muet1OpJXF6ofhc"
|
||||||
|
request methodGet "/authors_only" [auth] ""
|
||||||
|
`shouldRespondWith` 500
|
||||||
|
|
||||||
|
it "behaves normally when user does not attempt auth" $
|
||||||
|
request methodGet "/items" [] ""
|
||||||
|
`shouldRespondWith` 200
|
||||||
+16
-4
@@ -7,14 +7,17 @@ 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.IORef
|
import Data.IORef
|
||||||
import Data.String.Conversions (cs)
|
import Data.String.Conversions (cs)
|
||||||
|
import Data.Time.Clock.POSIX (getPOSIXTime)
|
||||||
|
|
||||||
import qualified Feature.AuthSpec
|
import qualified Feature.AuthSpec
|
||||||
import qualified Feature.ConcurrentSpec
|
import qualified Feature.ConcurrentSpec
|
||||||
import qualified Feature.CorsSpec
|
import qualified Feature.CorsSpec
|
||||||
import qualified Feature.DeleteSpec
|
import qualified Feature.DeleteSpec
|
||||||
import qualified Feature.InsertSpec
|
import qualified Feature.InsertSpec
|
||||||
|
import qualified Feature.NoJwtSpec
|
||||||
import qualified Feature.QueryLimitedSpec
|
import qualified Feature.QueryLimitedSpec
|
||||||
import qualified Feature.QuerySpec
|
import qualified Feature.QuerySpec
|
||||||
import qualified Feature.RangeSpec
|
import qualified Feature.RangeSpec
|
||||||
@@ -27,13 +30,18 @@ main = do
|
|||||||
setupDb
|
setupDb
|
||||||
|
|
||||||
pool <- P.acquire (3, 10, cs testDbConn)
|
pool <- P.acquire (3, 10, cs 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 $ either (error.show) id result
|
refDbStructure <- newIORef $ either (error.show) id result
|
||||||
let withApp = return $ postgrest testCfg refDbStructure pool
|
let withApp = return $ postgrest testCfg refDbStructure pool getTime
|
||||||
ltdApp = return $ postgrest testLtdRowsCfg refDbStructure pool
|
ltdApp = return $ postgrest testLtdRowsCfg refDbStructure pool getTime
|
||||||
unicodeApp = return $ postgrest testUnicodeCfg refDbStructure pool
|
unicodeApp = return $ postgrest testUnicodeCfg refDbStructure pool getTime
|
||||||
proxyApp = return $ postgrest testProxyCfg refDbStructure pool
|
proxyApp = return $ postgrest testProxyCfg refDbStructure pool getTime
|
||||||
|
noJwtApp = return $ postgrest testCfgNoJWT refDbStructure pool getTime
|
||||||
|
|
||||||
hspec $ do
|
hspec $ do
|
||||||
mapM_ (beforeAll_ resetDb . before withApp) specs
|
mapM_ (beforeAll_ resetDb . before withApp) specs
|
||||||
@@ -50,6 +58,10 @@ main = do
|
|||||||
beforeAll_ resetDb . before proxyApp $
|
beforeAll_ resetDb . before proxyApp $
|
||||||
describe "Feature.ProxySpec" Feature.ProxySpec.spec
|
describe "Feature.ProxySpec" Feature.ProxySpec.spec
|
||||||
|
|
||||||
|
-- this test runs without a JWT secret
|
||||||
|
beforeAll_ resetDb . before noJwtApp $
|
||||||
|
describe "Feature.NoJwtSpec" Feature.NoJwtSpec.spec
|
||||||
|
|
||||||
where
|
where
|
||||||
specs = map (uncurry describe) [
|
specs = map (uncurry describe) [
|
||||||
("Feature.AuthSpec" , Feature.AuthSpec.spec)
|
("Feature.AuthSpec" , Feature.AuthSpec.spec)
|
||||||
|
|||||||
+10
-5
@@ -8,7 +8,6 @@ import Data.CaseInsensitive (CI(..))
|
|||||||
import Text.Regex.TDFA ((=~))
|
import Text.Regex.TDFA ((=~))
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import System.Process (readProcess)
|
import System.Process (readProcess)
|
||||||
import Web.JWT (secret)
|
|
||||||
|
|
||||||
import PostgREST.Config (AppConfig(..))
|
import PostgREST.Config (AppConfig(..))
|
||||||
|
|
||||||
@@ -52,25 +51,31 @@ testDbConn = "postgres://postgrest_test_authenticator@localhost:5432/postgrest_t
|
|||||||
|
|
||||||
testCfg :: AppConfig
|
testCfg :: AppConfig
|
||||||
testCfg =
|
testCfg =
|
||||||
AppConfig testDbConn "postgrest_test_anonymous" Nothing "test" "localhost" 3000 (secret "safe") 10 Nothing True
|
AppConfig testDbConn "postgrest_test_anonymous" Nothing "test" "localhost" 3000 (Just "safe") 10 Nothing (Just "test.switch_role") True
|
||||||
|
|
||||||
|
testCfgNoJWT :: AppConfig
|
||||||
|
testCfgNoJWT =
|
||||||
|
AppConfig testDbConn "postgrest_test_anonymous" Nothing "test" "localhost" 3000 Nothing 10 Nothing Nothing True
|
||||||
|
|
||||||
testUnicodeCfg :: AppConfig
|
testUnicodeCfg :: AppConfig
|
||||||
testUnicodeCfg =
|
testUnicodeCfg =
|
||||||
AppConfig testDbConn "postgrest_test_anonymous" Nothing "تست" "localhost" 3000 (secret "safe") 10 Nothing True
|
AppConfig testDbConn "postgrest_test_anonymous" Nothing "تست" "localhost" 3000 (Just "safe") 10 Nothing Nothing True
|
||||||
|
|
||||||
testLtdRowsCfg :: AppConfig
|
testLtdRowsCfg :: AppConfig
|
||||||
testLtdRowsCfg =
|
testLtdRowsCfg =
|
||||||
AppConfig testDbConn "postgrest_test_anonymous" Nothing "test" "localhost" 3000 (secret "safe") 10 (Just 2) True
|
AppConfig testDbConn "postgrest_test_anonymous" Nothing "test" "localhost" 3000 (Just "safe") 10 (Just 2) Nothing True
|
||||||
|
|
||||||
testProxyCfg :: AppConfig
|
testProxyCfg :: AppConfig
|
||||||
testProxyCfg =
|
testProxyCfg =
|
||||||
AppConfig testDbConn "postgrest_test_anonymous" (Just "https://postgrest.com/openapi.json") "test" "localhost" 3000 (secret "safe") 10 Nothing True
|
AppConfig testDbConn "postgrest_test_anonymous" (Just "https://postgrest.com/openapi.json") "test" "localhost" 3000 (Just "safe") 10 Nothing Nothing True
|
||||||
|
|
||||||
setupDb :: IO ()
|
setupDb :: IO ()
|
||||||
setupDb = do
|
setupDb = do
|
||||||
void $ readProcess "psql" ["-d", "postgres", "-a", "-f", "test/fixtures/database.sql"] []
|
void $ readProcess "psql" ["-d", "postgres", "-a", "-f", "test/fixtures/database.sql"] []
|
||||||
|
void $ readProcess "psql" ["-d", "postgrest_test", "-a", "-c", "CREATE EXTENSION IF NOT EXISTS pgcrypto;"] []
|
||||||
loadFixture "roles"
|
loadFixture "roles"
|
||||||
loadFixture "schema"
|
loadFixture "schema"
|
||||||
|
loadFixture "jwt"
|
||||||
loadFixture "privileges"
|
loadFixture "privileges"
|
||||||
resetDb
|
resetDb
|
||||||
|
|
||||||
|
|||||||
Vendored
+2
@@ -2,3 +2,5 @@ DROP DATABASE IF EXISTS postgrest_test;
|
|||||||
DROP ROLE IF EXISTS postgrest_test;
|
DROP ROLE IF EXISTS postgrest_test;
|
||||||
CREATE USER postgrest_test createdb createrole;
|
CREATE USER postgrest_test createdb createrole;
|
||||||
CREATE DATABASE postgrest_test OWNER postgrest_test;
|
CREATE DATABASE postgrest_test OWNER postgrest_test;
|
||||||
|
|
||||||
|
ALTER DATABASE postgrest_test SET postgrest.claims.id = '-1';
|
||||||
|
|||||||
Vendored
+64
@@ -0,0 +1,64 @@
|
|||||||
|
-- From michelp/pgjwt commit c02bbd3
|
||||||
|
BEGIN;
|
||||||
|
DROP SCHEMA IF EXISTS jwt CASCADE;
|
||||||
|
CREATE SCHEMA jwt;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION jwt.url_encode(data bytea) RETURNS text LANGUAGE sql AS $$
|
||||||
|
SELECT translate(encode(data, 'base64'), E'+/=\n', '-_');
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION jwt.url_decode(data text) RETURNS bytea LANGUAGE sql AS $$
|
||||||
|
WITH t AS (SELECT translate(data, '-_', '+/')),
|
||||||
|
rem AS (SELECT length((SELECT * FROM t)) % 4) -- compute padding size
|
||||||
|
SELECT decode(
|
||||||
|
(SELECT * FROM t) ||
|
||||||
|
CASE WHEN (SELECT * FROM rem) > 0
|
||||||
|
THEN repeat('=', (4 - (SELECT * FROM rem)))
|
||||||
|
ELSE '' END,
|
||||||
|
'base64');
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION jwt.algorithm_sign(signables text, secret text, algorithm text)
|
||||||
|
RETURNS text LANGUAGE sql AS $$
|
||||||
|
WITH
|
||||||
|
alg AS (
|
||||||
|
SELECT CASE
|
||||||
|
WHEN algorithm = 'HS256' THEN 'sha256'
|
||||||
|
WHEN algorithm = 'HS384' THEN 'sha384'
|
||||||
|
WHEN algorithm = 'HS512' THEN 'sha512'
|
||||||
|
ELSE '' END) -- hmac throws error
|
||||||
|
SELECT jwt.url_encode(hmac(signables, secret, (select * FROM alg)));
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION jwt.sign(payload json, secret text, algorithm text DEFAULT 'HS256')
|
||||||
|
RETURNS text LANGUAGE sql AS $$
|
||||||
|
WITH
|
||||||
|
header AS (
|
||||||
|
SELECT jwt.url_encode(convert_to('{"alg":"' || algorithm || '","typ":"JWT"}', 'utf8'))
|
||||||
|
),
|
||||||
|
payload AS (
|
||||||
|
SELECT jwt.url_encode(convert_to(payload::text, 'utf8'))
|
||||||
|
),
|
||||||
|
signables AS (
|
||||||
|
SELECT (SELECT * FROM header) || '.' || (SELECT * FROM payload)
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
(SELECT * FROM signables)
|
||||||
|
|| '.' ||
|
||||||
|
jwt.algorithm_sign((SELECT * FROM signables), secret, algorithm);
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION jwt.verify(token text, secret text, algorithm text DEFAULT 'HS256')
|
||||||
|
RETURNS table(header json, payload json, valid boolean) LANGUAGE sql AS $$
|
||||||
|
SELECT
|
||||||
|
convert_from(jwt.url_decode(r[1]), 'utf8')::json AS header,
|
||||||
|
convert_from(jwt.url_decode(r[2]), 'utf8')::json AS payload,
|
||||||
|
r[3] = jwt.algorithm_sign(r[1] || '.' || r[2], secret, algorithm) AS valid
|
||||||
|
FROM regexp_split_to_array(token, '\.') r;
|
||||||
|
$$;
|
||||||
|
COMMIT;
|
||||||
Vendored
+1
@@ -2,6 +2,7 @@
|
|||||||
GRANT USAGE ON SCHEMA
|
GRANT USAGE ON SCHEMA
|
||||||
postgrest
|
postgrest
|
||||||
, test
|
, test
|
||||||
|
, jwt
|
||||||
, "تست"
|
, "تست"
|
||||||
TO postgrest_test_anonymous;
|
TO postgrest_test_anonymous;
|
||||||
|
|
||||||
|
|||||||
Vendored
+45
-26
@@ -49,29 +49,11 @@ CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;
|
|||||||
SET search_path = public, pg_catalog;
|
SET search_path = public, pg_catalog;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: jwt_claims; Type: TYPE; Schema: public; Owner: -
|
-- Name: jwt_token; Type: TYPE; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE TYPE jwt_claims AS (
|
CREATE TYPE jwt_token AS (
|
||||||
role text,
|
token text
|
||||||
id text
|
|
||||||
);
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: big_jwt_claims; Type: TYPE; Schema: public; Owner: -
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE TYPE big_jwt_claims AS (
|
|
||||||
iss text,
|
|
||||||
sub text,
|
|
||||||
aud text,
|
|
||||||
exp integer,
|
|
||||||
nbf integer,
|
|
||||||
iat integer,
|
|
||||||
jti text,
|
|
||||||
|
|
||||||
role text,
|
|
||||||
"http://postgrest.com/foo" boolean
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
@@ -208,10 +190,17 @@ $$;
|
|||||||
-- Name: login(text, text); Type: FUNCTION; Schema: test; Owner: -
|
-- Name: login(text, text); Type: FUNCTION; Schema: test; Owner: -
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE FUNCTION login(id text, pass text) RETURNS public.jwt_claims
|
CREATE FUNCTION login(id text, pass text) RETURNS public.jwt_token
|
||||||
LANGUAGE sql SECURITY DEFINER
|
LANGUAGE sql SECURITY DEFINER
|
||||||
AS $$
|
AS $$
|
||||||
SELECT rolname::text, id::text FROM postgrest.auth WHERE id = id AND pass = pass;
|
SELECT jwt.sign(
|
||||||
|
row_to_json(r), 'safe'
|
||||||
|
) as token
|
||||||
|
FROM (
|
||||||
|
SELECT rolname::text, id::text
|
||||||
|
FROM postgrest.auth
|
||||||
|
WHERE id = id AND pass = pass
|
||||||
|
) r;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
|
||||||
@@ -234,16 +223,46 @@ $_$;
|
|||||||
-- Name: jwt_test(); Type: FUNCTION; Schema: test; Owner: -
|
-- Name: jwt_test(); Type: FUNCTION; Schema: test; Owner: -
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE FUNCTION jwt_test() RETURNS public.big_jwt_claims
|
CREATE FUNCTION jwt_test() RETURNS public.jwt_token
|
||||||
LANGUAGE sql SECURITY DEFINER
|
LANGUAGE sql SECURITY DEFINER
|
||||||
AS $$
|
AS $$
|
||||||
SELECT 'joe'::text as iss, 'fun'::text as sub, 'everyone'::text as aud,
|
SELECT jwt.sign(
|
||||||
|
row_to_json(r), 'safe'
|
||||||
|
) as token
|
||||||
|
FROM (
|
||||||
|
SELECT 'joe'::text as iss, 'fun'::text as sub, 'everyone'::text as aud,
|
||||||
1300819380 as exp, 1300819380 as nbf, 1300819380 as iat,
|
1300819380 as exp, 1300819380 as nbf, 1300819380 as iat,
|
||||||
'foo'::text as jti, 'postgrest_test'::text as role,
|
'foo'::text as jti, 'postgrest_test'::text as role,
|
||||||
true as "http://postgrest.com/foo";
|
true as "http://postgrest.com/foo"
|
||||||
|
) r;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION switch_role() RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
declare
|
||||||
|
user_id text;
|
||||||
|
begin
|
||||||
|
user_id = current_setting('postgrest.claims.id')::text;
|
||||||
|
if user_id = '1'::text then
|
||||||
|
execute 'set local role postgrest_test_author';
|
||||||
|
elseif user_id = '2'::text then
|
||||||
|
execute 'set local role postgrest_test_default_role';
|
||||||
|
elseif user_id = '3'::text then
|
||||||
|
RAISE EXCEPTION 'Disabled ID --> %', user_id USING HINT = 'Please contact administrator';
|
||||||
|
/* else */
|
||||||
|
/* execute 'set local role postgrest_test_anonymous'; */
|
||||||
|
end if;
|
||||||
|
end
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE FUNCTION get_current_user() RETURNS text
|
||||||
|
LANGUAGE sql
|
||||||
|
AS $$
|
||||||
|
SELECT current_user::text;
|
||||||
|
$$;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: reveal_big_jwt(); Type: FUNCTION; Schema: test; Owner: -
|
-- Name: reveal_big_jwt(); Type: FUNCTION; Schema: test; Owner: -
|
||||||
--
|
--
|
||||||
|
|||||||
Reference in New Issue
Block a user