Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e30bc63f49 | ||
|
|
a8f40c4908 | ||
|
|
75972e9ffe | ||
|
|
4ba6b1b30c | ||
|
|
a4927141ee | ||
|
|
1b353590ff | ||
|
|
394aa026d3 | ||
|
|
272e2e7535 | ||
|
|
ea153523d1 | ||
|
|
bf0a1173b5 | ||
|
|
3a28968f3c | ||
|
|
86aac1ead5 |
@@ -11,7 +11,7 @@ inputs:
|
|||||||
runs:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- uses: nixbuild/nix-quick-install-action@63ca48f939ee3b8d835f4126562537df0fee5b91 # v32
|
- uses: nixbuild/nix-quick-install-action@1f095fee853b33114486cfdeae62fa099cda35a9 # v33
|
||||||
with:
|
with:
|
||||||
nix_conf: |-
|
nix_conf: |-
|
||||||
always-allow-substitutes = true
|
always-allow-substitutes = true
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
- name: Run coverage (IO tests and Spec tests against PostgreSQL 15)
|
- name: Run coverage (IO tests and Spec tests against PostgreSQL 15)
|
||||||
run: postgrest-coverage
|
run: postgrest-coverage
|
||||||
- name: Upload coverage to codecov
|
- name: Upload coverage to codecov
|
||||||
uses: codecov/codecov-action@fdcc8476540edceab3de004e990f80d881c6cc00 # v5.5.0
|
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||||
with:
|
with:
|
||||||
files: ./coverage/codecov.json
|
files: ./coverage/codecov.json
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|||||||
@@ -5,6 +5,23 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## [13.0.7] - 2025-09-14
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Improve the `PGRST106` error when the requested schema is invalid by @laurenceisla in #4089
|
||||||
|
+ It now shows the invalid schema in the `message` field.
|
||||||
|
+ The exposed schemas are now listed in the `hint` instead of the `message` field.
|
||||||
|
- Improve error details of `PGRST301` error by @taimoorzaeem in #4051
|
||||||
|
|
||||||
|
## [13.0.6] - 2025-08-30
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix logging the Haskell type instead of the listener error message directly by @laurenceisla in #3588
|
||||||
|
- Fix format of `IPv6` address logged at PostgREST startup by @taimoorzaeem in #4291
|
||||||
|
- Fix empty enum in `preferParams` OpenAPI parameter by @laurenceisla in #4292
|
||||||
|
|
||||||
## [13.0.5] - 2025-08-24
|
## [13.0.5] - 2025-08-24
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
# The x86-64 is a single-static-binary image built via Nix, see:
|
# The x86-64 is a single-static-binary image built via Nix, see:
|
||||||
# nix/tools/docker/README.md
|
# nix/tools/docker/README.md
|
||||||
|
|
||||||
FROM ubuntu:noble@sha256:7c06e91f61fa88c08cc74f7e1b7c69ae24910d745357e0dfe1d2c0322aaf20f9 AS postgrest
|
FROM ubuntu:noble@sha256:9cbed754112939e914291337b5e554b07ad7c392491dba6daf25eef1332a22e8 AS postgrest
|
||||||
|
|
||||||
RUN apt-get update -y \
|
RUN apt-get update -y \
|
||||||
&& apt install -y --no-install-recommends libpq-dev zlib1g-dev jq gcc libnuma-dev \
|
&& apt install -y --no-install-recommends libpq-dev zlib1g-dev jq gcc libnuma-dev \
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
name: postgrest
|
name: postgrest
|
||||||
version: 13.0.5
|
version: 13.0.7
|
||||||
synopsis: REST API for any Postgres database
|
synopsis: REST API for any Postgres database
|
||||||
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
||||||
for tables, views, and functions, supporting all HTTP methods that security
|
for tables, views, and functions, supporting all HTTP methods that security
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ getAction resource schema method =
|
|||||||
getSchema :: AppConfig -> RequestHeaders -> ByteString -> Either ApiRequestError (Schema, Bool)
|
getSchema :: AppConfig -> RequestHeaders -> ByteString -> Either ApiRequestError (Schema, Bool)
|
||||||
getSchema AppConfig{configDbSchemas} hdrs method = do
|
getSchema AppConfig{configDbSchemas} hdrs method = do
|
||||||
case profile of
|
case profile of
|
||||||
Just p | p `notElem` configDbSchemas -> Left $ UnacceptableSchema $ toList configDbSchemas
|
Just p | p `notElem` configDbSchemas -> Left $ UnacceptableSchema p $ toList configDbSchemas
|
||||||
| otherwise -> Right (p, True)
|
| otherwise -> Right (p, True)
|
||||||
Nothing -> Right (defaultSchema, length configDbSchemas /= 1) -- if we have many schemas, assume the default schema was negotiated
|
Nothing -> Right (defaultSchema, length configDbSchemas /= 1) -- if we have many schemas, assume the default schema was negotiated
|
||||||
where
|
where
|
||||||
|
|||||||
+19
-18
@@ -50,7 +50,8 @@ import PostgREST.Auth.JwtCache (lookupJwtCache)
|
|||||||
import PostgREST.Auth.Types (AuthResult (..))
|
import PostgREST.Auth.Types (AuthResult (..))
|
||||||
import PostgREST.Config (AppConfig (..), FilterExp (..),
|
import PostgREST.Config (AppConfig (..), FilterExp (..),
|
||||||
JSPath, JSPathExp (..))
|
JSPath, JSPathExp (..))
|
||||||
import PostgREST.Error (Error (..), JwtError (..))
|
import PostgREST.Error (Error (..), JwtClaimsError (..),
|
||||||
|
JwtDecodeError (..), JwtError (..))
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ import Protolude
|
|||||||
-- JSON object of JWT claims.
|
-- JSON object of JWT claims.
|
||||||
parseToken :: AppConfig -> Maybe ByteString -> UTCTime -> ExceptT Error IO JSON.Value
|
parseToken :: AppConfig -> Maybe ByteString -> UTCTime -> ExceptT Error IO JSON.Value
|
||||||
parseToken _ Nothing _ = return JSON.emptyObject
|
parseToken _ Nothing _ = return JSON.emptyObject
|
||||||
parseToken _ (Just "") _ = throwE . JwtErr $ JwtDecodeError "Empty JWT is sent in Authorization header"
|
parseToken _ (Just "") _ = throwE . JwtErr $ JwtDecodeErr EmptyAuthHeader
|
||||||
parseToken AppConfig{..} (Just tkn) time = do
|
parseToken AppConfig{..} (Just tkn) time = do
|
||||||
secret <- liftEither . maybeToRight (JwtErr JwtSecretMissing) $ configJWKS
|
secret <- liftEither . maybeToRight (JwtErr JwtSecretMissing) $ configJWKS
|
||||||
tknWith3Parts <- liftEither $ hasThreeParts tkn
|
tknWith3Parts <- liftEither $ hasThreeParts tkn
|
||||||
@@ -69,33 +70,33 @@ parseToken AppConfig{..} (Just tkn) time = do
|
|||||||
hasThreeParts :: ByteString -> Either Error ByteString
|
hasThreeParts :: ByteString -> Either Error ByteString
|
||||||
hasThreeParts token = case length $ BS.split (BS.c2w '.') token of
|
hasThreeParts token = case length $ BS.split (BS.c2w '.') token of
|
||||||
3 -> Right token
|
3 -> Right token
|
||||||
n -> Left $ JwtErr $ JwtDecodeError ("Expected 3 parts in JWT; got " <> show n)
|
n -> Left $ JwtErr $ JwtDecodeErr $ UnexpectedParts n
|
||||||
jwtDecodeError :: JWT.JwtError -> JwtError
|
jwtDecodeError :: JWT.JwtError -> JwtError
|
||||||
-- The only errors we can get from JWT.decode function are:
|
-- The only errors we can get from JWT.decode function are:
|
||||||
-- BadAlgorithm
|
-- BadAlgorithm
|
||||||
-- KeyError
|
-- KeyError
|
||||||
-- BadCrypto
|
-- BadCrypto
|
||||||
jwtDecodeError (JWT.KeyError _) = JwtDecodeError "No suitable key or wrong key type"
|
jwtDecodeError (JWT.KeyError m) = JwtDecodeErr $ KeyError m
|
||||||
jwtDecodeError (JWT.BadAlgorithm _) = JwtDecodeError "Wrong or unsupported encoding algorithm"
|
jwtDecodeError (JWT.BadAlgorithm m) = JwtDecodeErr $ BadAlgorithm m
|
||||||
jwtDecodeError JWT.BadCrypto = JwtDecodeError "JWT cryptographic operation failed"
|
jwtDecodeError JWT.BadCrypto = JwtDecodeErr BadCrypto
|
||||||
-- Control never reaches here, the decode function only returns the above three
|
-- Control never reaches here, the decode function only returns the above three
|
||||||
jwtDecodeError _ = JwtDecodeError "JWT couldn't be decoded"
|
jwtDecodeError _ = JwtDecodeErr UnreachableDecodeError
|
||||||
|
|
||||||
verifyClaims :: JWT.JwtContent -> Either JwtError JSON.Value
|
verifyClaims :: JWT.JwtContent -> Either JwtError JSON.Value
|
||||||
verifyClaims (JWT.Jws (_, claims)) = case JSON.decodeStrict claims of
|
verifyClaims (JWT.Jws (_, claims)) = case JSON.decodeStrict claims of
|
||||||
Just jclaims@(JSON.Object mclaims) ->
|
Just jclaims@(JSON.Object mclaims) ->
|
||||||
verifyClaim mclaims "exp" isValidExpClaim "JWT expired" >>
|
verifyClaim mclaims "exp" isValidExpClaim JWTExpired >>
|
||||||
verifyClaim mclaims "nbf" isValidNbfClaim "JWT not yet valid" >>
|
verifyClaim mclaims "nbf" isValidNbfClaim JWTNotYetValid >>
|
||||||
verifyClaim mclaims "iat" isValidIatClaim "JWT issued at future" >>
|
verifyClaim mclaims "iat" isValidIatClaim JWTIssuedAtFuture >>
|
||||||
verifyClaim mclaims "aud" isValidAudClaim "JWT not in audience" >>
|
verifyClaim mclaims "aud" isValidAudClaim JWTNotInAudience >>
|
||||||
return jclaims
|
return jclaims
|
||||||
_ -> Left $ JwtClaimsError "Parsing claims failed"
|
_ -> Left $ JwtClaimsErr ParsingClaimsFailed
|
||||||
-- TODO: We could enable JWE support here (encrypted tokens)
|
-- TODO: We could enable JWE support here (encrypted tokens)
|
||||||
verifyClaims _ = Left $ JwtDecodeError "Unsupported token type"
|
verifyClaims _ = Left $ JwtDecodeErr UnsupportedTokenType
|
||||||
|
|
||||||
verifyClaim mclaims claim func err = do
|
verifyClaim mclaims claim func err = do
|
||||||
isValid <- maybe (Right True) func (KM.lookup claim mclaims)
|
isValid <- maybe (Right True) func (KM.lookup claim mclaims)
|
||||||
unless isValid $ Left $ JwtClaimsError err
|
unless isValid $ Left $ JwtClaimsErr err
|
||||||
|
|
||||||
allowedSkewSeconds = 30 :: Int64
|
allowedSkewSeconds = 30 :: Int64
|
||||||
now = floor . nominalDiffTimeToSeconds $ utcTimeToPOSIXSeconds time
|
now = floor . nominalDiffTimeToSeconds $ utcTimeToPOSIXSeconds time
|
||||||
@@ -104,15 +105,15 @@ parseToken AppConfig{..} (Just tkn) time = do
|
|||||||
|
|
||||||
isValidExpClaim :: JSON.Value -> Either JwtError Bool
|
isValidExpClaim :: JSON.Value -> Either JwtError Bool
|
||||||
isValidExpClaim (JSON.Number secs) = Right $ now <= (sciToInt secs + allowedSkewSeconds)
|
isValidExpClaim (JSON.Number secs) = Right $ now <= (sciToInt secs + allowedSkewSeconds)
|
||||||
isValidExpClaim _ = Left $ JwtClaimsError "The JWT 'exp' claim must be a number"
|
isValidExpClaim _ = Left $ JwtClaimsErr ExpClaimNotNumber
|
||||||
|
|
||||||
isValidNbfClaim :: JSON.Value -> Either JwtError Bool
|
isValidNbfClaim :: JSON.Value -> Either JwtError Bool
|
||||||
isValidNbfClaim (JSON.Number secs) = Right $ now >= (sciToInt secs - allowedSkewSeconds)
|
isValidNbfClaim (JSON.Number secs) = Right $ now >= (sciToInt secs - allowedSkewSeconds)
|
||||||
isValidNbfClaim _ = Left $ JwtClaimsError "The JWT 'nbf' claim must be a number"
|
isValidNbfClaim _ = Left $ JwtClaimsErr NbfClaimNotNumber
|
||||||
|
|
||||||
isValidIatClaim :: JSON.Value -> Either JwtError Bool
|
isValidIatClaim :: JSON.Value -> Either JwtError Bool
|
||||||
isValidIatClaim (JSON.Number secs) = Right $ now >= (sciToInt secs - allowedSkewSeconds)
|
isValidIatClaim (JSON.Number secs) = Right $ now >= (sciToInt secs - allowedSkewSeconds)
|
||||||
isValidIatClaim _ = Left $ JwtClaimsError "The JWT 'iat' claim must be a number"
|
isValidIatClaim _ = Left $ JwtClaimsErr IatClaimNotNumber
|
||||||
|
|
||||||
isValidAudClaim :: JSON.Value -> Either JwtError Bool
|
isValidAudClaim :: JSON.Value -> Either JwtError Bool
|
||||||
isValidAudClaim JSON.Null = Right True -- {"aud": null} is valid for all audiences
|
isValidAudClaim JSON.Null = Right True -- {"aud": null} is valid for all audiences
|
||||||
@@ -120,7 +121,7 @@ parseToken AppConfig{..} (Just tkn) time = do
|
|||||||
isValidAudClaim (JSON.Array arr)
|
isValidAudClaim (JSON.Array arr)
|
||||||
| null arr = Right True -- {"aud": []} is valid for all audiences
|
| null arr = Right True -- {"aud": []} is valid for all audiences
|
||||||
| allStrings arr = Right $ maybe True (\a -> JSON.String a `elem` arr) configJwtAudience
|
| allStrings arr = Right $ maybe True (\a -> JSON.String a `elem` arr) configJwtAudience
|
||||||
isValidAudClaim _ = Left $ JwtClaimsError "The JWT 'aud' claim must be a string or an array of strings"
|
isValidAudClaim _ = Left $ JwtClaimsErr AudClaimNotStringOrArray
|
||||||
|
|
||||||
parseClaims :: Monad m =>
|
parseClaims :: Monad m =>
|
||||||
AppConfig -> JSON.Value -> ExceptT Error m AuthResult
|
AppConfig -> JSON.Value -> ExceptT Error m AuthResult
|
||||||
|
|||||||
+61
-16
@@ -14,6 +14,8 @@ module PostgREST.Error
|
|||||||
, PgError(..)
|
, PgError(..)
|
||||||
, Error(..)
|
, Error(..)
|
||||||
, JwtError (..)
|
, JwtError (..)
|
||||||
|
, JwtDecodeError(..)
|
||||||
|
, JwtClaimsError(..)
|
||||||
, errorPayload
|
, errorPayload
|
||||||
, status
|
, status
|
||||||
) where
|
) where
|
||||||
@@ -86,7 +88,7 @@ data ApiRequestError
|
|||||||
| QueryParamError QPError
|
| QueryParamError QPError
|
||||||
| RelatedOrderNotToOne Text Text
|
| RelatedOrderNotToOne Text Text
|
||||||
| UnacceptableFilter Text
|
| UnacceptableFilter Text
|
||||||
| UnacceptableSchema [Text]
|
| UnacceptableSchema Text [Text]
|
||||||
| UnsupportedMethod ByteString
|
| UnsupportedMethod ByteString
|
||||||
| GucHeadersError
|
| GucHeadersError
|
||||||
| GucStatusError
|
| GucStatusError
|
||||||
@@ -194,7 +196,7 @@ instance ErrorBody ApiRequestError where
|
|||||||
message (InvalidBody errorMessage) = T.decodeUtf8 errorMessage
|
message (InvalidBody errorMessage) = T.decodeUtf8 errorMessage
|
||||||
message (InvalidRange _) = "Requested range not satisfiable"
|
message (InvalidRange _) = "Requested range not satisfiable"
|
||||||
message InvalidFilters = "Filters must include all and only primary key columns with 'eq' operators"
|
message InvalidFilters = "Filters must include all and only primary key columns with 'eq' operators"
|
||||||
message (UnacceptableSchema schemas) = "The schema must be one of the following: " <> T.intercalate ", " schemas
|
message (UnacceptableSchema sch _) = "Invalid schema: " <> sch
|
||||||
message (MediaTypeError cts) = "None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)
|
message (MediaTypeError cts) = "None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)
|
||||||
message (NotEmbedded resource) = "'" <> resource <> "' is not an embedded resource in this request"
|
message (NotEmbedded resource) = "'" <> resource <> "' is not an embedded resource in this request"
|
||||||
message GucHeadersError = "response.headers guc must be a JSON array composed of objects with a single key and a string value"
|
message GucHeadersError = "response.headers guc must be a JSON array composed of objects with a single key and a string value"
|
||||||
@@ -234,6 +236,7 @@ instance ErrorBody ApiRequestError where
|
|||||||
-- HINT: Maybe JSON.Value
|
-- HINT: Maybe JSON.Value
|
||||||
hint (NotEmbedded resource) = Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter."
|
hint (NotEmbedded resource) = Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter."
|
||||||
hint (PGRSTParseError raiseErr) = Just $ JSON.String $ pgrstParseErrorHint raiseErr
|
hint (PGRSTParseError raiseErr) = Just $ JSON.String $ pgrstParseErrorHint raiseErr
|
||||||
|
hint (UnacceptableSchema _ schemas) = Just $ JSON.String $ "Only the following schemas are exposed: " <> T.intercalate ", " schemas
|
||||||
|
|
||||||
hint _ = Nothing
|
hint _ = Nothing
|
||||||
|
|
||||||
@@ -647,10 +650,32 @@ data Error
|
|||||||
deriving Show
|
deriving Show
|
||||||
|
|
||||||
data JwtError
|
data JwtError
|
||||||
= JwtDecodeError Text
|
= JwtDecodeErr JwtDecodeError
|
||||||
| JwtSecretMissing
|
| JwtSecretMissing
|
||||||
| JwtTokenRequired
|
| JwtTokenRequired
|
||||||
| JwtClaimsError Text
|
| JwtClaimsErr JwtClaimsError
|
||||||
|
deriving Show
|
||||||
|
|
||||||
|
data JwtDecodeError
|
||||||
|
= EmptyAuthHeader
|
||||||
|
| UnexpectedParts Int
|
||||||
|
| KeyError Text
|
||||||
|
| BadAlgorithm Text
|
||||||
|
| BadCrypto
|
||||||
|
| UnsupportedTokenType
|
||||||
|
| UnreachableDecodeError
|
||||||
|
deriving Show
|
||||||
|
|
||||||
|
data JwtClaimsError
|
||||||
|
= JWTExpired
|
||||||
|
| JWTNotYetValid
|
||||||
|
| JWTIssuedAtFuture
|
||||||
|
| JWTNotInAudience
|
||||||
|
| ParsingClaimsFailed
|
||||||
|
| ExpClaimNotNumber
|
||||||
|
| NbfClaimNotNumber
|
||||||
|
| IatClaimNotNumber
|
||||||
|
| AudClaimNotStringOrArray
|
||||||
deriving Show
|
deriving Show
|
||||||
|
|
||||||
instance PgrstError Error where
|
instance PgrstError Error where
|
||||||
@@ -696,14 +721,14 @@ instance ErrorBody Error where
|
|||||||
hint (PgErr err) = hint err
|
hint (PgErr err) = hint err
|
||||||
|
|
||||||
instance PgrstError JwtError where
|
instance PgrstError JwtError where
|
||||||
status JwtDecodeError{} = HTTP.unauthorized401
|
status JwtDecodeErr{} = HTTP.unauthorized401
|
||||||
status JwtSecretMissing = HTTP.status500
|
status JwtSecretMissing = HTTP.status500
|
||||||
status JwtTokenRequired = HTTP.unauthorized401
|
status JwtTokenRequired = HTTP.unauthorized401
|
||||||
status JwtClaimsError{} = HTTP.unauthorized401
|
status JwtClaimsErr{} = HTTP.unauthorized401
|
||||||
|
|
||||||
headers (JwtDecodeError m) = [invalidTokenHeader m]
|
headers e@(JwtDecodeErr _) = [invalidTokenHeader $ message e]
|
||||||
headers JwtTokenRequired = [requiredTokenHeader]
|
headers JwtTokenRequired = [requiredTokenHeader]
|
||||||
headers (JwtClaimsError m) = [invalidTokenHeader m]
|
headers e@(JwtClaimsErr _) = [invalidTokenHeader $ message e]
|
||||||
headers _ = mempty
|
headers _ = mempty
|
||||||
|
|
||||||
instance JSON.ToJSON JwtError where
|
instance JSON.ToJSON JwtError where
|
||||||
@@ -711,16 +736,36 @@ instance JSON.ToJSON JwtError where
|
|||||||
(code err) (message err) (details err) (hint err)
|
(code err) (message err) (details err) (hint err)
|
||||||
|
|
||||||
instance ErrorBody JwtError where
|
instance ErrorBody JwtError where
|
||||||
code JwtSecretMissing = "PGRST300"
|
code JwtSecretMissing = "PGRST300"
|
||||||
code (JwtDecodeError _) = "PGRST301"
|
code (JwtDecodeErr _) = "PGRST301"
|
||||||
code JwtTokenRequired = "PGRST302"
|
code JwtTokenRequired = "PGRST302"
|
||||||
code (JwtClaimsError _) = "PGRST303"
|
code (JwtClaimsErr _) = "PGRST303"
|
||||||
|
|
||||||
message JwtSecretMissing = "Server lacks JWT secret"
|
message JwtSecretMissing = "Server lacks JWT secret"
|
||||||
message (JwtDecodeError msg) = msg
|
message (JwtDecodeErr e) = case e of
|
||||||
message JwtTokenRequired = "Anonymous access is disabled"
|
EmptyAuthHeader -> "Empty JWT is sent in Authorization header"
|
||||||
message (JwtClaimsError msg) = msg
|
UnexpectedParts n -> "Expected 3 parts in JWT; got " <> show n
|
||||||
|
KeyError _ -> "No suitable key or wrong key type"
|
||||||
|
BadAlgorithm _ -> "Wrong or unsupported encoding algorithm"
|
||||||
|
BadCrypto -> "JWT cryptographic operation failed"
|
||||||
|
UnsupportedTokenType -> "Unsupported token type"
|
||||||
|
UnreachableDecodeError -> "JWT couldn't be decoded"
|
||||||
|
message JwtTokenRequired = "Anonymous access is disabled"
|
||||||
|
message (JwtClaimsErr e) = case e of
|
||||||
|
JWTExpired -> "JWT expired"
|
||||||
|
JWTNotYetValid -> "JWT not yet valid"
|
||||||
|
JWTIssuedAtFuture -> "JWT issued at future"
|
||||||
|
JWTNotInAudience -> "JWT not in audience"
|
||||||
|
ParsingClaimsFailed -> "Parsing claims failed"
|
||||||
|
ExpClaimNotNumber -> "The JWT 'exp' claim must be a number"
|
||||||
|
NbfClaimNotNumber -> "The JWT 'nbf' claim must be a number"
|
||||||
|
IatClaimNotNumber -> "The JWT 'iat' claim must be a number"
|
||||||
|
AudClaimNotStringOrArray -> "The JWT 'aud' claim must be a string or an array of strings"
|
||||||
|
|
||||||
|
details (JwtDecodeErr jde) = case jde of
|
||||||
|
KeyError dets -> Just $ JSON.String dets
|
||||||
|
BadAlgorithm dets -> Just $ JSON.String dets
|
||||||
|
_ -> Nothing
|
||||||
details _ = Nothing
|
details _ = Nothing
|
||||||
|
|
||||||
hint _ = Nothing
|
hint _ = Nothing
|
||||||
|
|||||||
@@ -13,5 +13,9 @@ resolveHost sock = do
|
|||||||
sn <- NS.getSocketName sock
|
sn <- NS.getSocketName sock
|
||||||
case sn of
|
case sn of
|
||||||
NS.SockAddrInet _ hostAddr -> pure $ Just $ fromString $ show $ fromHostAddress hostAddr
|
NS.SockAddrInet _ hostAddr -> pure $ Just $ fromString $ show $ fromHostAddress hostAddr
|
||||||
NS.SockAddrInet6 _ _ hostAddr6 _ -> pure $ Just $ fromString $ show $ fromHostAddress6 hostAddr6
|
-- The IPv6 addresses are wrapped in [] brackets. This is done in accordance
|
||||||
|
-- to RFC 3986 (https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2).
|
||||||
|
-- In short, we did this to have a clear separation between the port and host
|
||||||
|
-- because the components of an IPv6 are separated with the ':' character.
|
||||||
|
NS.SockAddrInet6 _ _ hostAddr6 _ -> pure $ Just $ fromString $ "[" ++ show (fromHostAddress6 hostAddr6) ++ "]"
|
||||||
_ -> pure Nothing
|
_ -> pure Nothing
|
||||||
|
|||||||
@@ -108,11 +108,8 @@ observationMessage = \case
|
|||||||
DBListenStart channel -> do
|
DBListenStart channel -> do
|
||||||
"Listening for database notifications on the " <> show channel <> " channel"
|
"Listening for database notifications on the " <> show channel <> " channel"
|
||||||
DBListenFail channel listenErr ->
|
DBListenFail channel listenErr ->
|
||||||
"Failed listening for database notifications on the " <> show channel <> " channel. " <> (
|
"Failed listening for database notifications on the " <> show channel <> " channel. " <>
|
||||||
case listenErr of
|
either showListenerConnError showListenerException listenErr
|
||||||
Left err -> show err
|
|
||||||
Right err -> showListenerError err
|
|
||||||
)
|
|
||||||
DBListenRetry delay ->
|
DBListenRetry delay ->
|
||||||
"Retrying listening for database notifications in " <> (show delay::Text) <> " seconds..."
|
"Retrying listening for database notifications in " <> (show delay::Text) <> " seconds..."
|
||||||
DBListenerGotSCacheMsg channel ->
|
DBListenerGotSCacheMsg channel ->
|
||||||
@@ -157,8 +154,11 @@ observationMessage = \case
|
|||||||
|
|
||||||
jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.PgError False err
|
jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.PgError False err
|
||||||
|
|
||||||
showListenerError :: Either SomeException () -> Text
|
showOnSingleLine txt = T.intercalate " " $ T.filter (/= '\t') <$> T.lines txt -- the errors from hasql-notifications come intercalated with "\t\n"
|
||||||
showListenerError (Right _) = "Failed getting notifications" -- should not happen as the listener will never finish (hasql-notifications uses `forever` internally) with a Right result
|
|
||||||
showListenerError (Left e) =
|
showListenerConnError :: SQL.ConnectionError -> Text
|
||||||
let showOnSingleLine txt = T.intercalate " " $ T.filter (/= '\t') <$> T.lines txt in -- the errors from hasql-notifications come intercalated with "\t\n"
|
showListenerConnError = maybe "Connection error" (showOnSingleLine . T.decodeUtf8)
|
||||||
showOnSingleLine $ show e
|
|
||||||
|
showListenerException :: Either SomeException () -> Text
|
||||||
|
showListenerException (Right _) = "Failed getting notifications" -- should not happen as the listener will never finish (hasql-notifications uses `forever` internally) with a Right result
|
||||||
|
showListenerException (Left e) = showOnSingleLine $ show e
|
||||||
|
|||||||
@@ -171,8 +171,9 @@ makePreferParam ts =
|
|||||||
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||||
& in_ .~ ParamHeader
|
& in_ .~ ParamHeader
|
||||||
& type_ ?~ SwaggerString
|
& type_ ?~ SwaggerString
|
||||||
& enum_ .~ JSON.decode (JSON.encode $ foldl (<>) [] (val <$> ts)))
|
& enum_ .~ if null enu then Nothing else JSON.decode (JSON.encode enu))
|
||||||
where
|
where
|
||||||
|
enu = foldl (<>) [] (val <$> ts)
|
||||||
val :: Text -> [Text]
|
val :: Text -> [Text]
|
||||||
val = \case
|
val = \case
|
||||||
"count" -> ["count=none"]
|
"count" -> ["count=none"]
|
||||||
|
|||||||
+13
-2
@@ -96,7 +96,9 @@ def run(
|
|||||||
if port:
|
if port:
|
||||||
env["PGRST_SERVER_PORT"] = str(port)
|
env["PGRST_SERVER_PORT"] = str(port)
|
||||||
env["PGRST_SERVER_HOST"] = host or "localhost"
|
env["PGRST_SERVER_HOST"] = host or "localhost"
|
||||||
baseurl = f"http://localhost:{port}"
|
# When constructing IPv6 address, host address should be bracketed like [host]
|
||||||
|
apihost = f"[{host}]" if host and is_ipv6(host) else "localhost"
|
||||||
|
baseurl = f"http://{apihost}:{port}"
|
||||||
else:
|
else:
|
||||||
socketfile = pathlib.Path(tmpdir) / "postgrest.sock"
|
socketfile = pathlib.Path(tmpdir) / "postgrest.sock"
|
||||||
env["PGRST_SERVER_UNIX_SOCKET"] = str(socketfile)
|
env["PGRST_SERVER_UNIX_SOCKET"] = str(socketfile)
|
||||||
@@ -104,7 +106,8 @@ def run(
|
|||||||
|
|
||||||
adminport = freeport(port)
|
adminport = freeport(port)
|
||||||
env["PGRST_ADMIN_SERVER_PORT"] = str(adminport)
|
env["PGRST_ADMIN_SERVER_PORT"] = str(adminport)
|
||||||
adminurl = f"http://localhost:{adminport}"
|
adminhost = f"[{host}]" if host and is_ipv6(host) else "localhost"
|
||||||
|
adminurl = f"http://{adminhost}:{adminport}"
|
||||||
|
|
||||||
command = [POSTGREST_BIN]
|
command = [POSTGREST_BIN]
|
||||||
env["HPCTIXFILE"] = hpctixfile()
|
env["HPCTIXFILE"] = hpctixfile()
|
||||||
@@ -218,3 +221,11 @@ def sleep_pool_connection(url, seconds):
|
|||||||
session.get(url + f"/rpc/sleep?seconds={seconds}", timeout=0.1)
|
session.get(url + f"/rpc/sleep?seconds={seconds}", timeout=0.1)
|
||||||
except requests.exceptions.ReadTimeout:
|
except requests.exceptions.ReadTimeout:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def is_ipv6(addr):
|
||||||
|
try:
|
||||||
|
socket.inet_pton(socket.AF_INET6, addr)
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|||||||
+31
-3
@@ -93,6 +93,9 @@ def test_jwt_errors(defaultenv):
|
|||||||
response = postgrest.session.get("/", headers=headers)
|
response = postgrest.session.get("/", headers=headers)
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
assert response.json()["message"] == "No suitable key or wrong key type"
|
assert response.json()["message"] == "No suitable key or wrong key type"
|
||||||
|
assert (
|
||||||
|
response.json()["details"] == "None of the keys was able to decode the JWT"
|
||||||
|
)
|
||||||
|
|
||||||
headers = jwtauthheader({"role": "not_existing"}, SECRET)
|
headers = jwtauthheader({"role": "not_existing"}, SECRET)
|
||||||
response = postgrest.session.get("/", headers=headers)
|
response = postgrest.session.get("/", headers=headers)
|
||||||
@@ -141,6 +144,10 @@ def test_jwt_errors(defaultenv):
|
|||||||
response = postgrest.session.get("/", headers=headers)
|
response = postgrest.session.get("/", headers=headers)
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
assert response.json()["message"] == "Wrong or unsupported encoding algorithm"
|
assert response.json()["message"] == "Wrong or unsupported encoding algorithm"
|
||||||
|
assert (
|
||||||
|
response.json()["details"]
|
||||||
|
== "JWT is unsecured but expected 'alg' was not 'none'"
|
||||||
|
)
|
||||||
|
|
||||||
env = {
|
env = {
|
||||||
**defaultenv,
|
**defaultenv,
|
||||||
@@ -1355,9 +1362,9 @@ def test_log_postgrest_version(defaultenv):
|
|||||||
assert "Starting PostgREST %s..." % version in output[0]
|
assert "Starting PostgREST %s..." % version in output[0]
|
||||||
|
|
||||||
|
|
||||||
def test_log_postgrest_host_and_port(defaultenv):
|
@pytest.mark.parametrize("host", ["127.0.0.1", "::1"])
|
||||||
|
def test_log_postgrest_host_and_port(host, defaultenv):
|
||||||
"PostgREST should output the host and port it is bound to."
|
"PostgREST should output the host and port it is bound to."
|
||||||
host = "127.0.0.1"
|
|
||||||
port = freeport()
|
port = freeport()
|
||||||
|
|
||||||
with run(
|
with run(
|
||||||
@@ -1365,7 +1372,10 @@ def test_log_postgrest_host_and_port(defaultenv):
|
|||||||
) as postgrest:
|
) as postgrest:
|
||||||
output = postgrest.read_stdout(nlines=10)
|
output = postgrest.read_stdout(nlines=10)
|
||||||
|
|
||||||
assert f"API server listening on {host}:{port}" in output[2] # output-sensitive
|
if is_ipv6(host): # IPv6
|
||||||
|
assert f"API server listening on [{host}]:{port}" in output[2]
|
||||||
|
else: # IPv4
|
||||||
|
assert f"API server listening on {host}:{port}" in output[2]
|
||||||
|
|
||||||
|
|
||||||
def test_succeed_w_role_having_superuser_settings(defaultenv):
|
def test_succeed_w_role_having_superuser_settings(defaultenv):
|
||||||
@@ -1930,3 +1940,21 @@ def test_schema_cache_error_observation(defaultenv):
|
|||||||
"Failed to load the schema cache using db-schemas=public and db-extra-search-path=x"
|
"Failed to load the schema cache using db-schemas=public and db-extra-search-path=x"
|
||||||
in output[7]
|
in output[7]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_log_listener_connection_errors(defaultenv):
|
||||||
|
"The logs should show the listener connection error message in a single line"
|
||||||
|
|
||||||
|
env = {
|
||||||
|
**defaultenv,
|
||||||
|
"PGHOST": "no_host",
|
||||||
|
"PGRST_DB_CHANNEL_ENABLED": "true",
|
||||||
|
}
|
||||||
|
|
||||||
|
with run(env=env, no_startup_stdout=False, wait_for_readiness=False) as postgrest:
|
||||||
|
output = postgrest.read_stdout(nlines=5)
|
||||||
|
assert any(
|
||||||
|
'Failed listening for database notifications on the "pgrst" channel. could not translate host name "no_host" to address:'
|
||||||
|
in line
|
||||||
|
for line in output
|
||||||
|
)
|
||||||
|
|||||||
@@ -1085,6 +1085,13 @@ spec = describe "OpenAPI" $ do
|
|||||||
immutableGet `shouldNotBe` Nothing
|
immutableGet `shouldNotBe` Nothing
|
||||||
immutablePost `shouldNotBe` Nothing
|
immutablePost `shouldNotBe` Nothing
|
||||||
|
|
||||||
|
it "does not include empty enum in the preferParams parameter" $ do
|
||||||
|
r <- simpleBody <$> get "/"
|
||||||
|
let preferParams = r ^? key "parameters" . key "preferParams" . key "enum"
|
||||||
|
|
||||||
|
liftIO $ do
|
||||||
|
preferParams `shouldBe` Nothing
|
||||||
|
|
||||||
describe "Security" $
|
describe "Security" $
|
||||||
it "does not include security or security definitions by default" $ do
|
it "does not include security or security definitions by default" $ do
|
||||||
r <- simpleBody <$> get "/"
|
r <- simpleBody <$> get "/"
|
||||||
|
|||||||
@@ -72,9 +72,9 @@ spec =
|
|||||||
, matchHeaders = []
|
, matchHeaders = []
|
||||||
}
|
}
|
||||||
|
|
||||||
it "fails trying to read table from unkown schema" $
|
it "fails trying to read table from unknown schema" $
|
||||||
request methodGet "/parents" [("Accept-Profile", "unkown")] "" `shouldRespondWith`
|
request methodGet "/parents" [("Accept-Profile", "unknown")] "" `shouldRespondWith`
|
||||||
[json|{"message":"The schema must be one of the following: v1, v2, SPECIAL \"@/\\#~_-","code":"PGRST106","details":null,"hint":null}|]
|
[json|{"message":"Invalid schema: unknown","code":"PGRST106","details":null,"hint":"Only the following schemas are exposed: v1, v2, SPECIAL \"@/\\#~_-"}|]
|
||||||
{
|
{
|
||||||
matchStatus = 406
|
matchStatus = 406
|
||||||
}
|
}
|
||||||
@@ -151,7 +151,7 @@ spec =
|
|||||||
request methodPost "/children" [("Content-Profile", "unknown")]
|
request methodPost "/children" [("Content-Profile", "unknown")]
|
||||||
[json|{"name": "child 4", "parent_id": 4}|]
|
[json|{"name": "child 4", "parent_id": 4}|]
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"message":"The schema must be one of the following: v1, v2, SPECIAL \"@/\\#~_-","code":"PGRST106","details":null,"hint":null}|]
|
[json|{"message":"Invalid schema: unknown","code":"PGRST106","details":null,"hint":"Only the following schemas are exposed: v1, v2, SPECIAL \"@/\\#~_-"}|]
|
||||||
{
|
{
|
||||||
matchStatus = 406
|
matchStatus = 406
|
||||||
}
|
}
|
||||||
@@ -389,9 +389,9 @@ spec =
|
|||||||
let def = simpleBody r ^? key "definitions" . key "another_table"
|
let def = simpleBody r ^? key "definitions" . key "another_table"
|
||||||
def `shouldBe` Nothing
|
def `shouldBe` Nothing
|
||||||
|
|
||||||
it "fails trying to read definitions from unkown schema" $
|
it "fails trying to read definitions from unknown schema" $
|
||||||
request methodGet "/" [("Accept-Profile", "unkown")] "" `shouldRespondWith`
|
request methodGet "/" [("Accept-Profile", "unknown")] "" `shouldRespondWith`
|
||||||
[json|{"message":"The schema must be one of the following: v1, v2, SPECIAL \"@/\\#~_-","code":"PGRST106","details":null,"hint":null}|]
|
[json|{"message":"Invalid schema: unknown","code":"PGRST106","details":null,"hint":"Only the following schemas are exposed: v1, v2, SPECIAL \"@/\\#~_-"}|]
|
||||||
{
|
{
|
||||||
matchStatus = 406
|
matchStatus = 406
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user