feat: Make db-anon-role optional
Without db-anon-role, PostgREST will block any anonymous access without hitting the database. Resolves #1689, Ref #1823
This commit is contained in:
@@ -208,7 +208,7 @@ postgrestResponse conf@AppConfig{..} maybeDbStructure jsonDbS pgVer pool AuthRes
|
||||
|
||||
let handleReq apiReq = handleRequest $ RequestContext conf dbStructure apiReq pgVer
|
||||
|
||||
runDbHandler pool (txMode apiRequest) (authRole /= configDbAnonRole) configDbPreparedStatements .
|
||||
runDbHandler pool (txMode apiRequest) (Just authRole /= configDbAnonRole) configDbPreparedStatements .
|
||||
Middleware.optionalRollback conf apiRequest $
|
||||
Middleware.runPgLocals conf authClaims authRole handleReq apiRequest jsonDbS pgVer
|
||||
|
||||
|
||||
+16
-15
@@ -32,7 +32,7 @@ import qualified Network.Wai.Middleware.HttpAuth as Wai
|
||||
|
||||
import Control.Lens (set)
|
||||
import Control.Monad.Except (liftEither)
|
||||
import Data.Either.Combinators (mapLeft, mapRight)
|
||||
import Data.Either.Combinators (mapLeft)
|
||||
import Data.List (lookup)
|
||||
import Data.Time.Clock (UTCTime)
|
||||
import System.IO.Unsafe (unsafePerformIO)
|
||||
@@ -71,16 +71,17 @@ parseToken AppConfig{..} token time = do
|
||||
jwtClaimsError JWT.JWTExpired = JwtTokenInvalid "JWT expired"
|
||||
jwtClaimsError e = JwtTokenInvalid $ show e
|
||||
|
||||
parseClaims :: AppConfig -> JSON.Value -> AuthResult
|
||||
parseClaims AppConfig{..} jclaims@(JSON.Object mclaims) =
|
||||
AuthResult
|
||||
{ authClaims = mclaims & M.insert "role" (JSON.toJSON role)
|
||||
, authRole = role
|
||||
}
|
||||
parseClaims :: Monad m =>
|
||||
AppConfig -> JSON.Value -> ExceptT Error m AuthResult
|
||||
parseClaims AppConfig{..} jclaims@(JSON.Object mclaims) = do
|
||||
-- role defaults to anon if not specified in jwt
|
||||
role <- liftEither . maybeToRight JwtTokenRequired $
|
||||
unquoted <$> walkJSPath (Just jclaims) configJwtRoleClaimKey <|> configDbAnonRole
|
||||
return AuthResult
|
||||
{ authClaims = mclaims & M.insert "role" (JSON.toJSON role)
|
||||
, authRole = role
|
||||
}
|
||||
where
|
||||
-- role defaults to anon if not specified in jwt
|
||||
role = maybe configDbAnonRole unquoted (walkJSPath (Just jclaims) configJwtRoleClaimKey)
|
||||
|
||||
walkJSPath :: Maybe JSON.Value -> JSPath -> Maybe JSON.Value
|
||||
walkJSPath x [] = x
|
||||
walkJSPath (Just (JSON.Object o)) (JSPKey key:rest) = walkJSPath (M.lookup key o) rest
|
||||
@@ -91,7 +92,7 @@ parseClaims AppConfig{..} jclaims@(JSON.Object mclaims) =
|
||||
unquoted (JSON.String t) = t
|
||||
unquoted v = T.decodeUtf8 . LBS.toStrict $ JSON.encode v
|
||||
-- impossible case - just added to please -Wincomplete-patterns
|
||||
parseClaims _ _ = AuthResult { authClaims = M.empty, authRole = mempty }
|
||||
parseClaims _ _ = return AuthResult { authClaims = M.empty, authRole = mempty }
|
||||
|
||||
-- | Validate authorization header.
|
||||
-- Parse and store JWT claims for future use in the request.
|
||||
@@ -101,11 +102,11 @@ middleware appState app req respond = do
|
||||
time <- getTime appState
|
||||
|
||||
let token = fromMaybe "" $ Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req)
|
||||
claims <- runExceptT $ parseToken conf (LBS.fromStrict token) time
|
||||
authResult <- runExceptT $
|
||||
parseToken conf (LBS.fromStrict token) time >>=
|
||||
parseClaims conf
|
||||
|
||||
let
|
||||
authResult = mapRight (parseClaims conf) claims
|
||||
req' = req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
|
||||
let req' = req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
|
||||
app req' respond
|
||||
|
||||
authResultKey :: Vault.Key (Either Error AuthResult)
|
||||
|
||||
@@ -63,7 +63,7 @@ import Protolude hiding (Proxy, toList)
|
||||
|
||||
data AppConfig = AppConfig
|
||||
{ configAppSettings :: [(Text, Text)]
|
||||
, configDbAnonRole :: Text
|
||||
, configDbAnonRole :: Maybe Text
|
||||
, configDbChannel :: Text
|
||||
, configDbChannelEnabled :: Bool
|
||||
, configDbExtraSearchPath :: [Text]
|
||||
@@ -121,7 +121,7 @@ toText conf =
|
||||
where
|
||||
-- apply conf to all pgrst settings
|
||||
pgrstSettings = (\(k, v) -> (k, v conf)) <$>
|
||||
[("db-anon-role", q . configDbAnonRole)
|
||||
[("db-anon-role", q . fromMaybe "" . configDbAnonRole)
|
||||
,("db-channel", q . configDbChannel)
|
||||
,("db-channel-enabled", T.toLower . show . configDbChannelEnabled)
|
||||
,("db-extra-search-path", q . T.intercalate "," . configDbExtraSearchPath)
|
||||
@@ -207,7 +207,7 @@ parser :: Maybe FilePath -> Environment -> [(Text, Text)] -> C.Parser C.Config A
|
||||
parser optPath env dbSettings =
|
||||
AppConfig
|
||||
<$> parseAppSettings "app.settings"
|
||||
<*> reqString "db-anon-role"
|
||||
<*> optString "db-anon-role"
|
||||
<*> (fromMaybe "pgrst" <$> optString "db-channel")
|
||||
<*> (fromMaybe True <$> optBool "db-channel-enabled")
|
||||
<*> (maybe ["public"] splitOnCommas <$> optValue "db-extra-search-path")
|
||||
@@ -322,9 +322,6 @@ parser optPath env dbSettings =
|
||||
Just v -> pure $ Just v
|
||||
Nothing -> alias
|
||||
|
||||
reqString :: C.Key -> C.Parser C.Config Text
|
||||
reqString k = overrideFromDbOrEnvironment C.required k coerceText
|
||||
|
||||
optString :: C.Key -> C.Parser C.Config (Maybe Text)
|
||||
optString k = mfilter (/= "") <$> overrideFromDbOrEnvironment C.optional k coerceText
|
||||
|
||||
@@ -352,7 +349,7 @@ parser optPath env dbSettings =
|
||||
let dbSettingName = T.pack $ dashToUnderscore <$> toS key in
|
||||
if dbSettingName `notElem` [
|
||||
"server_host", "server_port", "server_unix_socket", "server_unix_socket_mode", "log_level",
|
||||
"db_anon_role", "db_uri", "db_channel_enabled", "db_channel", "db_pool", "db_pool_timeout", "db_config"]
|
||||
"db_uri", "db_channel_enabled", "db_channel", "db_pool", "db_pool_timeout", "db_config"]
|
||||
then lookup dbSettingName dbSettings
|
||||
else Nothing
|
||||
|
||||
|
||||
@@ -274,6 +274,7 @@ data Error
|
||||
| PutRangeNotAllowedError
|
||||
| JwtTokenMissing
|
||||
| JwtTokenInvalid Text
|
||||
| JwtTokenRequired
|
||||
| SingularityError Integer
|
||||
| NotFound
|
||||
| ApiRequestError ApiRequestError
|
||||
@@ -288,6 +289,7 @@ instance PgrstError Error where
|
||||
status PutRangeNotAllowedError = HTTP.status400
|
||||
status JwtTokenMissing = HTTP.status500
|
||||
status (JwtTokenInvalid _) = HTTP.unauthorized401
|
||||
status JwtTokenRequired = HTTP.unauthorized401
|
||||
status (SingularityError _) = HTTP.status406
|
||||
status NotFound = HTTP.status404
|
||||
status (PgErr err) = status err
|
||||
@@ -295,6 +297,7 @@ instance PgrstError Error where
|
||||
|
||||
headers (SingularityError _) = [ContentType.toHeader CTSingularJSON]
|
||||
headers (JwtTokenInvalid m) = [ContentType.toHeader CTApplicationJSON, invalidTokenHeader m]
|
||||
headers JwtTokenRequired = [ContentType.toHeader CTApplicationJSON, requiredTokenHeader]
|
||||
headers (PgErr err) = headers err
|
||||
headers (ApiRequestError err) = headers err
|
||||
headers _ = [ContentType.toHeader CTApplicationJSON]
|
||||
@@ -322,6 +325,8 @@ instance JSON.ToJSON Error where
|
||||
"message" .= ("Server lacks JWT secret" :: Text)]
|
||||
toJSON (JwtTokenInvalid message) = JSON.object [
|
||||
"message" .= (message :: Text)]
|
||||
toJSON JwtTokenRequired = JSON.object [
|
||||
"message" .= ("Anonymous access is disabled" :: Text)]
|
||||
toJSON NotFound = JSON.object []
|
||||
toJSON (PgErr err) = JSON.toJSON err
|
||||
toJSON (ApiRequestError err) = JSON.toJSON err
|
||||
@@ -330,5 +335,8 @@ invalidTokenHeader :: Text -> Header
|
||||
invalidTokenHeader m =
|
||||
("WWW-Authenticate", "Bearer error=\"invalid_token\", " <> "error_description=" <> encodeUtf8 (show m))
|
||||
|
||||
requiredTokenHeader :: Header
|
||||
requiredTokenHeader = ("WWW-Authenticate", "Bearer")
|
||||
|
||||
singularityError :: (Integral a) => a -> Error
|
||||
singularityError = SingularityError . toInteger
|
||||
|
||||
Reference in New Issue
Block a user