feat: add config to specify CORS origins (#2986)

This commit is contained in:
Taimoor Zaeem
2023-10-24 09:38:25 -05:00
committed by GitHub
parent 618f93dec1
commit d94286c185
19 changed files with 134 additions and 52 deletions
+1
View File
@@ -13,6 +13,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #2698, Add config `jwt-cache-max-lifetime` and implement JWT caching - @taimoorzaeem - #2698, Add config `jwt-cache-max-lifetime` and implement JWT caching - @taimoorzaeem
- #2943, Add `handling=strict/lenient` for Prefer header - @taimoorzaeem - #2943, Add `handling=strict/lenient` for Prefer header - @taimoorzaeem
- #2983, Add more data to `Server-Timing` header - @develop7 - #2983, Add more data to `Server-Timing` header - @develop7
- #2441, Add config `server-cors-allowed-origins` to specify CORS origins - @taimoorzaeem
### Fixed ### Fixed
+1 -1
View File
@@ -110,7 +110,7 @@ serverSettings AppConfig{..} =
postgrest :: AppConfig -> AppState.AppState -> IO () -> Wai.Application postgrest :: AppConfig -> AppState.AppState -> IO () -> Wai.Application
postgrest conf appState connWorker = postgrest conf appState connWorker =
traceHeaderMiddleware conf . traceHeaderMiddleware conf .
Cors.middleware . Cors.middleware (configServerCorsAllowedOrigins conf) .
Auth.middleware appState . Auth.middleware appState .
Logger.middleware (configLogLevel conf) $ Logger.middleware (configLogLevel conf) $
-- fromJust can be used, because the auth middleware will **always** add -- fromJust can be used, because the auth middleware will **always** add
+3
View File
@@ -221,6 +221,9 @@ exampleConfigFile =
|## Content types to produce raw output |## Content types to produce raw output
|# raw-media-types="image/png, image/jpg" |# raw-media-types="image/png, image/jpg"
| |
|## Configurable CORS origins
|# server-cors-allowed-origins = ""
|
|server-host = "!4" |server-host = "!4"
|server-port = 3000 |server-port = 3000
| |
+3
View File
@@ -103,6 +103,7 @@ data AppConfig = AppConfig
, configOpenApiSecurityActive :: Bool , configOpenApiSecurityActive :: Bool
, configOpenApiServerProxyUri :: Maybe Text , configOpenApiServerProxyUri :: Maybe Text
, configRawMediaTypes :: [MediaType] , configRawMediaTypes :: [MediaType]
, configServerCorsAllowedOrigins :: Maybe [Text]
, configServerHost :: Text , configServerHost :: Text
, configServerPort :: Int , configServerPort :: Int
, configServerTraceHeader :: Maybe (CI.CI BS.ByteString) , configServerTraceHeader :: Maybe (CI.CI BS.ByteString)
@@ -169,6 +170,7 @@ toText conf =
,("openapi-security-active", T.toLower . show . configOpenApiSecurityActive) ,("openapi-security-active", T.toLower . show . configOpenApiSecurityActive)
,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri) ,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri)
,("raw-media-types", q . T.decodeUtf8 . BS.intercalate "," . fmap toMime . configRawMediaTypes) ,("raw-media-types", q . T.decodeUtf8 . BS.intercalate "," . fmap toMime . configRawMediaTypes)
,("server-cors-allowed-origins", q . maybe "" (T.intercalate ",") . configServerCorsAllowedOrigins)
,("server-host", q . configServerHost) ,("server-host", q . configServerHost)
,("server-port", show . configServerPort) ,("server-port", show . configServerPort)
,("server-trace-header", q . T.decodeUtf8 . maybe mempty CI.original . configServerTraceHeader) ,("server-trace-header", q . T.decodeUtf8 . maybe mempty CI.original . configServerTraceHeader)
@@ -273,6 +275,7 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
<*> (fromMaybe False <$> optBool "openapi-security-active") <*> (fromMaybe False <$> optBool "openapi-security-active")
<*> parseOpenAPIServerProxyURI "openapi-server-proxy-uri" <*> parseOpenAPIServerProxyURI "openapi-server-proxy-uri"
<*> (maybe [] (fmap (MTOther . encodeUtf8) . splitOnCommas) <$> optValue "raw-media-types") <*> (maybe [] (fmap (MTOther . encodeUtf8) . splitOnCommas) <$> optValue "raw-media-types")
<*> (fmap splitOnCommas <$> optValue "server-cors-allowed-origins")
<*> (fromMaybe "!4" <$> optString "server-host") <*> (fromMaybe "!4" <$> optString "server-host")
<*> (fromMaybe 3000 <$> optInt "server-port") <*> (fromMaybe 3000 <$> optInt "server-port")
<*> (fmap (CI.mk . encodeUtf8) <$> optString "server-trace-header") <*> (fmap (CI.mk . encodeUtf8) <$> optString "server-trace-header")
+10 -6
View File
@@ -2,10 +2,14 @@
Module : PostgREST.Cors Module : PostgREST.Cors
Description : Wai Middleware to set cors policy. Description : Wai Middleware to set cors policy.
-} -}
{-# LANGUAGE TupleSections #-}
module PostgREST.Cors (middleware) where module PostgREST.Cors (middleware) where
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import qualified Data.Text.Encoding as T
import qualified Network.Wai as Wai import qualified Network.Wai as Wai
import qualified Network.Wai.Middleware.Cors as Wai import qualified Network.Wai.Middleware.Cors as Wai
@@ -13,15 +17,15 @@ import Data.List (lookup)
import Protolude import Protolude
middleware :: Wai.Middleware middleware :: Maybe [Text] -> Wai.Middleware
middleware = Wai.cors corsPolicy middleware corsAllowedOrigins = Wai.cors $ corsPolicy corsAllowedOrigins
-- | CORS policy to be used in by Wai Cors middleware -- | CORS policy to be used in by Wai Cors middleware
corsPolicy :: Wai.Request -> Maybe Wai.CorsResourcePolicy corsPolicy :: Maybe [Text] -> Wai.Request -> Maybe Wai.CorsResourcePolicy
corsPolicy req = case lookup "origin" headers of corsPolicy corsAllowedOrigins req = case lookup "origin" headers of
Just origin -> Just _ ->
Just Wai.CorsResourcePolicy Just Wai.CorsResourcePolicy
{ Wai.corsOrigins = Just ([origin], True) { Wai.corsOrigins = (, True) . map T.encodeUtf8 <$> corsAllowedOrigins
, Wai.corsMethods = ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"] , Wai.corsMethods = ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"]
, Wai.corsRequestHeaders = "Authorization" : accHeaders , Wai.corsRequestHeaders = "Authorization" : accHeaders
, Wai.corsExposedHeaders = Just , Wai.corsExposedHeaders = Just
+1
View File
@@ -28,6 +28,7 @@ openapi-mode = "follow-privileges"
openapi-security-active = false openapi-security-active = false
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
raw-media-types = "" raw-media-types = ""
server-cors-allowed-origins = ""
server-host = "!4" server-host = "!4"
server-port = 3000 server-port = 3000
server-trace-header = "" server-trace-header = ""
@@ -28,6 +28,7 @@ openapi-mode = "follow-privileges"
openapi-security-active = false openapi-security-active = false
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
raw-media-types = "" raw-media-types = ""
server-cors-allowed-origins = ""
server-host = "!4" server-host = "!4"
server-port = 3000 server-port = 3000
server-trace-header = "" server-trace-header = ""
@@ -28,6 +28,7 @@ openapi-mode = "follow-privileges"
openapi-security-active = false openapi-security-active = false
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
raw-media-types = "" raw-media-types = ""
server-cors-allowed-origins = ""
server-host = "!4" server-host = "!4"
server-port = 3000 server-port = 3000
server-trace-header = "" server-trace-header = ""
+1
View File
@@ -28,6 +28,7 @@ openapi-mode = "follow-privileges"
openapi-security-active = false openapi-security-active = false
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
raw-media-types = "" raw-media-types = ""
server-cors-allowed-origins = ""
server-host = "!4" server-host = "!4"
server-port = 3000 server-port = 3000
server-trace-header = "" server-trace-header = ""
@@ -28,6 +28,7 @@ openapi-mode = "disabled"
openapi-security-active = false openapi-security-active = false
openapi-server-proxy-uri = "https://otherexample.org/api" openapi-server-proxy-uri = "https://otherexample.org/api"
raw-media-types = "application/vnd.pgrst.other-db-config" raw-media-types = "application/vnd.pgrst.other-db-config"
server-cors-allowed-origins = "http://example.com"
server-host = "0.0.0.0" server-host = "0.0.0.0"
server-port = 80 server-port = 80
server-trace-header = "traceparent" server-trace-header = "traceparent"
@@ -28,6 +28,7 @@ openapi-mode = "ignore-privileges"
openapi-security-active = true openapi-security-active = true
openapi-server-proxy-uri = "https://example.org/api" openapi-server-proxy-uri = "https://example.org/api"
raw-media-types = "application/vnd.pgrst.db-config" raw-media-types = "application/vnd.pgrst.db-config"
server-cors-allowed-origins = "http://example.com"
server-host = "0.0.0.0" server-host = "0.0.0.0"
server-port = 80 server-port = 80
server-trace-header = "CF-Ray" server-trace-header = "CF-Ray"
@@ -28,6 +28,7 @@ openapi-mode = "ignore-privileges"
openapi-security-active = true openapi-security-active = true
openapi-server-proxy-uri = "https://postgrest.org" openapi-server-proxy-uri = "https://postgrest.org"
raw-media-types = "application/vnd.pgrst.config" raw-media-types = "application/vnd.pgrst.config"
server-cors-allowed-origins = "http://example.com"
server-host = "0.0.0.0" server-host = "0.0.0.0"
server-port = 80 server-port = 80
server-trace-header = "X-Request-Id" server-trace-header = "X-Request-Id"
+1
View File
@@ -28,6 +28,7 @@ openapi-mode = "follow-privileges"
openapi-security-active = false openapi-security-active = false
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
raw-media-types = "" raw-media-types = ""
server-cors-allowed-origins = ""
server-host = "!4" server-host = "!4"
server-port = 3000 server-port = 3000
server-trace-header = "" server-trace-header = ""
+1
View File
@@ -30,6 +30,7 @@ PGRST_OPENAPI_MODE: 'ignore-privileges'
PGRST_OPENAPI_SECURITY_ACTIVE: true PGRST_OPENAPI_SECURITY_ACTIVE: true
PGRST_OPENAPI_SERVER_PROXY_URI: 'https://postgrest.org' PGRST_OPENAPI_SERVER_PROXY_URI: 'https://postgrest.org'
PGRST_RAW_MEDIA_TYPES: application/vnd.pgrst.config PGRST_RAW_MEDIA_TYPES: application/vnd.pgrst.config
PGRST_SERVER_CORS_ALLOWED_ORIGINS: "http://example.com"
PGRST_SERVER_HOST: 0.0.0.0 PGRST_SERVER_HOST: 0.0.0.0
PGRST_SERVER_PORT: 80 PGRST_SERVER_PORT: 80
PGRST_SERVER_TRACE_HEADER: X-Request-Id PGRST_SERVER_TRACE_HEADER: X-Request-Id
+1
View File
@@ -28,6 +28,7 @@ openapi-mode = "ignore-privileges"
openapi-security-active = true openapi-security-active = true
openapi-server-proxy-uri = "https://postgrest.org" openapi-server-proxy-uri = "https://postgrest.org"
raw-media-types = "application/vnd.pgrst.config" raw-media-types = "application/vnd.pgrst.config"
server-cors-allowed-origins = "http://example.com"
server-host = "0.0.0.0" server-host = "0.0.0.0"
server-port = 80 server-port = 80
server-trace-header = "X-Request-Id" server-trace-header = "X-Request-Id"
+2
View File
@@ -18,6 +18,7 @@ ALTER ROLE db_config_authenticator SET pgrst.db_pre_request = 'test.custom_heade
ALTER ROLE db_config_authenticator SET pgrst.db_max_rows = '1000'; ALTER ROLE db_config_authenticator SET pgrst.db_max_rows = '1000';
ALTER ROLE db_config_authenticator SET pgrst.db_extra_search_path = 'public, extensions'; ALTER ROLE db_config_authenticator SET pgrst.db_extra_search_path = 'public, extensions';
ALTER ROLE db_config_authenticator SET pgrst.not_existing = 'should be ignored'; ALTER ROLE db_config_authenticator SET pgrst.not_existing = 'should be ignored';
ALTER ROLE db_config_authenticator SET pgrst.server_cors_allowed_origins = 'http://example.com';
ALTER ROLE db_config_authenticator SET pgrst.server_trace_header = 'CF-Ray'; ALTER ROLE db_config_authenticator SET pgrst.server_trace_header = 'CF-Ray';
-- override with database specific setting -- override with database specific setting
@@ -62,6 +63,7 @@ ALTER ROLE other_authenticator SET pgrst.db_max_rows = '100';
ALTER ROLE other_authenticator SET pgrst.db_extra_search_path = 'public, extensions, other'; ALTER ROLE other_authenticator SET pgrst.db_extra_search_path = 'public, extensions, other';
ALTER ROLE other_authenticator SET pgrst.openapi_mode = 'disabled'; ALTER ROLE other_authenticator SET pgrst.openapi_mode = 'disabled';
ALTER ROLE other_authenticator SET pgrst.openapi_security_active = 'false'; ALTER ROLE other_authenticator SET pgrst.openapi_security_active = 'false';
ALTER ROLE other_authenticator SET pgrst.server_cors_allowed_origins = 'http://example.com';
ALTER ROLE other_authenticator SET pgrst.server_trace_header = 'traceparent'; ALTER ROLE other_authenticator SET pgrst.server_trace_header = 'traceparent';
ALTER ROLE other_authenticator SET pgrst.db_pre_config = 'postgrest.pre_config'; ALTER ROLE other_authenticator SET pgrst.db_pre_config = 'postgrest.pre_config';
+59
View File
@@ -1214,3 +1214,62 @@ def test_jwt_cache_with_no_exp_claim(defaultenv):
# their difference should be atleast 300, implying # their difference should be atleast 300, implying
# that JWT Caching is working as expected # that JWT Caching is working as expected
assert (first_dur - second_dur) > 300.0 assert (first_dur - second_dur) > 300.0
def test_preflight_request_with_cors_allowed_origin_config(defaultenv):
"OPTIONS preflight request should return Access-Control-Allow-Origin equal to origin"
env = {
**defaultenv,
"PGRST_SERVER_CORS_ALLOWED_ORIGINS": "http://example.com, http://example2.com",
}
headers = {
"Accept": "*/*",
"Origin": "http://example.com",
"Access-Control-Request-Method": "POST",
"Access-Control-Request-Headers": "Content-Type",
}
with run(env=env) as postgrest:
response = postgrest.session.options("/items", headers=headers)
assert (
response.headers["Access-Control-Allow-Origin"] == "http://example.com"
and response.headers["Access-Control-Allow-Credentials"] == "true"
)
def test_no_preflight_request_with_CORS_config_should_return_header(defaultenv):
"GET no preflight request should return Access-Control-Allow-Origin equal to origin"
env = {
**defaultenv,
"PGRST_SERVER_CORS_ALLOWED_ORIGINS": "http://example.com, http://example2.com",
}
headers = {
"Accept": "*/*",
"Origin": "http://example.com",
}
with run(env=env) as postgrest:
response = postgrest.session.get("/items", headers=headers)
assert response.headers["Access-Control-Allow-Origin"] == "http://example.com"
def test_no_preflight_request_with_CORS_config_should_not_return_header(defaultenv):
"GET no preflight request should not return Access-Control-Allow-Origin"
env = {
**defaultenv,
"PGRST_SERVER_CORS_ALLOWED_ORIGINS": "http://example.com, http://example2.com",
}
headers = {
"Accept": "*/*",
"Origin": "http://invalid.com",
}
with run(env=env) as postgrest:
response = postgrest.session.get("/items", headers=headers)
assert "Access-Control-Allow-Origin" not in response.headers
+1 -2
View File
@@ -20,8 +20,7 @@ spec =
"" ""
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchHeaders = [ "Access-Control-Allow-Origin" <:> "http://example.com" { matchHeaders = [ "Access-Control-Allow-Origin" <:> "*"
, "Access-Control-Allow-Credentials" <:> "true"
, "Access-Control-Allow-Methods" <:> "GET, POST, PATCH, PUT, DELETE, OPTIONS, HEAD" , "Access-Control-Allow-Methods" <:> "GET, POST, PATCH, PUT, DELETE, OPTIONS, HEAD"
, "Access-Control-Allow-Headers" <:> "Authorization, Foo, Bar, Accept, Accept-Language, Content-Language" , "Access-Control-Allow-Headers" <:> "Authorization, Foo, Bar, Accept, Accept-Language, Content-Language"
, "Access-Control-Max-Age" <:> "86400" ] , "Access-Control-Max-Age" <:> "86400" ]
+44 -43
View File
@@ -97,49 +97,50 @@ validateOpenApiResponse headers = do
baseCfg :: AppConfig baseCfg :: AppConfig
baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
AppConfig { AppConfig {
configAppSettings = [ ("app.settings.app_host", "localhost") , ("app.settings.external_api_secret", "0123456789abcdef") ] configAppSettings = [ ("app.settings.app_host", "localhost") , ("app.settings.external_api_secret", "0123456789abcdef") ]
, configDbAnonRole = Just "postgrest_test_anonymous" , configDbAnonRole = Just "postgrest_test_anonymous"
, configDbChannel = mempty , configDbChannel = mempty
, configDbChannelEnabled = True , configDbChannelEnabled = True
, configDbExtraSearchPath = [] , configDbExtraSearchPath = []
, configDbMaxRows = Nothing , configDbMaxRows = Nothing
, configDbPlanEnabled = False , configDbPlanEnabled = False
, configDbPoolSize = 10 , configDbPoolSize = 10
, configDbPoolAcquisitionTimeout = 10 , configDbPoolAcquisitionTimeout = 10
, configDbPoolMaxLifetime = 1800 , configDbPoolMaxLifetime = 1800
, configDbPoolMaxIdletime = 600 , configDbPoolMaxIdletime = 600
, configDbPoolAutomaticRecovery = True , configDbPoolAutomaticRecovery = True
, configDbPreRequest = Just $ QualifiedIdentifier "test" "switch_role" , configDbPreRequest = Just $ QualifiedIdentifier "test" "switch_role"
, configDbPreparedStatements = True , configDbPreparedStatements = True
, configDbRootSpec = Nothing , configDbRootSpec = Nothing
, configDbSchemas = fromList ["test"] , configDbSchemas = fromList ["test"]
, configDbConfig = False , configDbConfig = False
, configDbPreConfig = Nothing , configDbPreConfig = Nothing
, configDbUri = "postgresql://" , configDbUri = "postgresql://"
, configDbUseLegacyGucs = True , configDbUseLegacyGucs = True
, configFilePath = Nothing , configFilePath = Nothing
, configJWKS = parseSecret <$> secret , configJWKS = parseSecret <$> secret
, configJwtAudience = Nothing , configJwtAudience = Nothing
, configJwtRoleClaimKey = [JSPKey "role"] , configJwtRoleClaimKey = [JSPKey "role"]
, configJwtSecret = secret , configJwtSecret = secret
, configJwtSecretIsBase64 = False , configJwtSecretIsBase64 = False
, configJwtCacheMaxLifetime = 0 , configJwtCacheMaxLifetime = 0
, configLogLevel = LogCrit , configLogLevel = LogCrit
, configOpenApiMode = OAFollowPriv , configOpenApiMode = OAFollowPriv
, configOpenApiSecurityActive = False , configOpenApiSecurityActive = False
, configOpenApiServerProxyUri = Nothing , configOpenApiServerProxyUri = Nothing
, configRawMediaTypes = [] , configRawMediaTypes = []
, configServerHost = "localhost" , configServerCorsAllowedOrigins = Nothing
, configServerPort = 3000 , configServerHost = "localhost"
, configServerTraceHeader = Nothing , configServerPort = 3000
, configServerUnixSocket = Nothing , configServerTraceHeader = Nothing
, configServerUnixSocketMode = 432 , configServerUnixSocket = Nothing
, configDbTxAllowOverride = True , configServerUnixSocketMode = 432
, configDbTxRollbackAll = True , configDbTxAllowOverride = True
, configAdminServerPort = Nothing , configDbTxRollbackAll = True
, configRoleSettings = mempty , configAdminServerPort = Nothing
, configRoleIsoLvl = mempty , configRoleSettings = mempty
, configInternalSCSleep = Nothing , configRoleIsoLvl = mempty
, configInternalSCSleep = Nothing
} }
testCfg :: AppConfig testCfg :: AppConfig