Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add config to specify CORS origins #2986

Merged
merged 2 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
- #2943, Add `handling=strict/lenient` for Prefer header - @taimoorzaeem
- #2983, Add more data to `Server-Timing` header - @develop7
- #2441, Add config `server-cors-allowed-origins` to specify CORS origins - @taimoorzaeem

### Fixed

Expand Down
2 changes: 1 addition & 1 deletion src/PostgREST/App.hs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ serverSettings AppConfig{..} =
postgrest :: AppConfig -> AppState.AppState -> IO () -> Wai.Application
postgrest conf appState connWorker =
traceHeaderMiddleware conf .
Cors.middleware .
Cors.middleware (configServerCorsAllowedOrigins conf) .
Auth.middleware appState .
Logger.middleware (configLogLevel conf) $
-- fromJust can be used, because the auth middleware will **always** add
Expand Down
3 changes: 3 additions & 0 deletions src/PostgREST/CLI.hs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ exampleConfigFile =
|## Content types to produce raw output
|# raw-media-types="image/png, image/jpg"
|
|## Configurable CORS origins
|# server-cors-allowed-origins = ""
|
|server-host = "!4"
|server-port = 3000
|
Expand Down
3 changes: 3 additions & 0 deletions src/PostgREST/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ data AppConfig = AppConfig
, configOpenApiSecurityActive :: Bool
, configOpenApiServerProxyUri :: Maybe Text
, configRawMediaTypes :: [MediaType]
, configServerCorsAllowedOrigins :: Maybe [Text]
, configServerHost :: Text
, configServerPort :: Int
, configServerTraceHeader :: Maybe (CI.CI BS.ByteString)
Expand Down Expand Up @@ -169,6 +170,7 @@ toText conf =
,("openapi-security-active", T.toLower . show . configOpenApiSecurityActive)
,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri)
,("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-port", show . configServerPort)
,("server-trace-header", q . T.decodeUtf8 . maybe mempty CI.original . configServerTraceHeader)
Expand Down Expand Up @@ -273,6 +275,7 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
<*> (fromMaybe False <$> optBool "openapi-security-active")
<*> parseOpenAPIServerProxyURI "openapi-server-proxy-uri"
<*> (maybe [] (fmap (MTOther . encodeUtf8) . splitOnCommas) <$> optValue "raw-media-types")
<*> (fmap splitOnCommas <$> optValue "server-cors-allowed-origins")
<*> (fromMaybe "!4" <$> optString "server-host")
<*> (fromMaybe 3000 <$> optInt "server-port")
<*> (fmap (CI.mk . encodeUtf8) <$> optString "server-trace-header")
Expand Down
16 changes: 10 additions & 6 deletions src/PostgREST/Cors.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,30 @@
Module : PostgREST.Cors
Description : Wai Middleware to set cors policy.
-}

{-# LANGUAGE TupleSections #-}

module PostgREST.Cors (middleware) where

import qualified Data.ByteString.Char8 as BS
import qualified Data.CaseInsensitive as CI
import qualified Data.Text.Encoding as T
import qualified Network.Wai as Wai
import qualified Network.Wai.Middleware.Cors as Wai

import Data.List (lookup)

import Protolude

middleware :: Wai.Middleware
middleware = Wai.cors corsPolicy
middleware :: Maybe [Text] -> Wai.Middleware
middleware corsAllowedOrigins = Wai.cors $ corsPolicy corsAllowedOrigins

-- | CORS policy to be used in by Wai Cors middleware
corsPolicy :: Wai.Request -> Maybe Wai.CorsResourcePolicy
corsPolicy req = case lookup "origin" headers of
Just origin ->
corsPolicy :: Maybe [Text] -> Wai.Request -> Maybe Wai.CorsResourcePolicy
corsPolicy corsAllowedOrigins req = case lookup "origin" headers of
Just _ ->
Just Wai.CorsResourcePolicy
{ Wai.corsOrigins = Just ([origin], True)
{ Wai.corsOrigins = (, True) . map T.encodeUtf8 <$> corsAllowedOrigins
, Wai.corsMethods = ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"]
, Wai.corsRequestHeaders = "Authorization" : accHeaders
, Wai.corsExposedHeaders = Just
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/aliases.config
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ openapi-mode = "follow-privileges"
openapi-security-active = false
openapi-server-proxy-uri = ""
raw-media-types = ""
server-cors-allowed-origins = ""
server-host = "!4"
server-port = 3000
server-trace-header = ""
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/boolean-numeric.config
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ openapi-mode = "follow-privileges"
openapi-security-active = false
openapi-server-proxy-uri = ""
raw-media-types = ""
server-cors-allowed-origins = ""
server-host = "!4"
server-port = 3000
server-trace-header = ""
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/boolean-string.config
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ openapi-mode = "follow-privileges"
openapi-security-active = false
openapi-server-proxy-uri = ""
raw-media-types = ""
server-cors-allowed-origins = ""
server-host = "!4"
server-port = 3000
server-trace-header = ""
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/defaults.config
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ openapi-mode = "follow-privileges"
openapi-security-active = false
openapi-server-proxy-uri = ""
raw-media-types = ""
server-cors-allowed-origins = ""
server-host = "!4"
server-port = 3000
server-trace-header = ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ openapi-mode = "disabled"
openapi-security-active = false
openapi-server-proxy-uri = "https://otherexample.org/api"
raw-media-types = "application/vnd.pgrst.other-db-config"
server-cors-allowed-origins = "http://example.com"
server-host = "0.0.0.0"
server-port = 80
server-trace-header = "traceparent"
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/no-defaults-with-db.config
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ openapi-mode = "ignore-privileges"
openapi-security-active = true
openapi-server-proxy-uri = "https://example.org/api"
raw-media-types = "application/vnd.pgrst.db-config"
server-cors-allowed-origins = "http://example.com"
server-host = "0.0.0.0"
server-port = 80
server-trace-header = "CF-Ray"
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/no-defaults.config
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ openapi-mode = "ignore-privileges"
openapi-security-active = true
openapi-server-proxy-uri = "https://postgrest.org"
raw-media-types = "application/vnd.pgrst.config"
server-cors-allowed-origins = "http://example.com"
server-host = "0.0.0.0"
server-port = 80
server-trace-header = "X-Request-Id"
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/types.config
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ openapi-mode = "follow-privileges"
openapi-security-active = false
openapi-server-proxy-uri = ""
raw-media-types = ""
server-cors-allowed-origins = ""
server-host = "!4"
server-port = 3000
server-trace-header = ""
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/no-defaults-env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ PGRST_OPENAPI_MODE: 'ignore-privileges'
PGRST_OPENAPI_SECURITY_ACTIVE: true
PGRST_OPENAPI_SERVER_PROXY_URI: 'https://postgrest.org'
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_PORT: 80
PGRST_SERVER_TRACE_HEADER: X-Request-Id
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/no-defaults.config
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ openapi-mode = "ignore-privileges"
openapi-security-active = true
openapi-server-proxy-uri = "https://postgrest.org"
raw-media-types = "application/vnd.pgrst.config"
server-cors-allowed-origins = "http://example.com"
server-host = "0.0.0.0"
server-port = 80
server-trace-header = "X-Request-Id"
Expand Down
2 changes: 2 additions & 0 deletions test/io/db_config.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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_extra_search_path = 'public, extensions';
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';

-- override with database specific setting
Expand Down Expand Up @@ -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.openapi_mode = 'disabled';
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.db_pre_config = 'postgrest.pre_config';

Expand Down
59 changes: 59 additions & 0 deletions test/io/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -1214,3 +1214,62 @@ def test_jwt_cache_with_no_exp_claim(defaultenv):
# their difference should be atleast 300, implying
# that JWT Caching is working as expected
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
3 changes: 1 addition & 2 deletions test/spec/Feature/CorsSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ spec =
""
`shouldRespondWith`
""
{ matchHeaders = [ "Access-Control-Allow-Origin" <:> "http://example.com"
, "Access-Control-Allow-Credentials" <:> "true"
{ matchHeaders = [ "Access-Control-Allow-Origin" <:> "*"
, "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-Max-Age" <:> "86400" ]
Expand Down
87 changes: 44 additions & 43 deletions test/spec/SpecHelper.hs
Original file line number Diff line number Diff line change
Expand Up @@ -97,49 +97,50 @@ validateOpenApiResponse headers = do
baseCfg :: AppConfig
baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
AppConfig {
configAppSettings = [ ("app.settings.app_host", "localhost") , ("app.settings.external_api_secret", "0123456789abcdef") ]
, configDbAnonRole = Just "postgrest_test_anonymous"
, configDbChannel = mempty
, configDbChannelEnabled = True
, configDbExtraSearchPath = []
, configDbMaxRows = Nothing
, configDbPlanEnabled = False
, configDbPoolSize = 10
, configDbPoolAcquisitionTimeout = 10
, configDbPoolMaxLifetime = 1800
, configDbPoolMaxIdletime = 600
, configDbPoolAutomaticRecovery = True
, configDbPreRequest = Just $ QualifiedIdentifier "test" "switch_role"
, configDbPreparedStatements = True
, configDbRootSpec = Nothing
, configDbSchemas = fromList ["test"]
, configDbConfig = False
, configDbPreConfig = Nothing
, configDbUri = "postgresql://"
, configDbUseLegacyGucs = True
, configFilePath = Nothing
, configJWKS = parseSecret <$> secret
, configJwtAudience = Nothing
, configJwtRoleClaimKey = [JSPKey "role"]
, configJwtSecret = secret
, configJwtSecretIsBase64 = False
, configJwtCacheMaxLifetime = 0
, configLogLevel = LogCrit
, configOpenApiMode = OAFollowPriv
, configOpenApiSecurityActive = False
, configOpenApiServerProxyUri = Nothing
, configRawMediaTypes = []
, configServerHost = "localhost"
, configServerPort = 3000
, configServerTraceHeader = Nothing
, configServerUnixSocket = Nothing
, configServerUnixSocketMode = 432
, configDbTxAllowOverride = True
, configDbTxRollbackAll = True
, configAdminServerPort = Nothing
, configRoleSettings = mempty
, configRoleIsoLvl = mempty
, configInternalSCSleep = Nothing
configAppSettings = [ ("app.settings.app_host", "localhost") , ("app.settings.external_api_secret", "0123456789abcdef") ]
, configDbAnonRole = Just "postgrest_test_anonymous"
, configDbChannel = mempty
, configDbChannelEnabled = True
, configDbExtraSearchPath = []
, configDbMaxRows = Nothing
, configDbPlanEnabled = False
, configDbPoolSize = 10
, configDbPoolAcquisitionTimeout = 10
, configDbPoolMaxLifetime = 1800
, configDbPoolMaxIdletime = 600
, configDbPoolAutomaticRecovery = True
, configDbPreRequest = Just $ QualifiedIdentifier "test" "switch_role"
, configDbPreparedStatements = True
, configDbRootSpec = Nothing
, configDbSchemas = fromList ["test"]
, configDbConfig = False
, configDbPreConfig = Nothing
, configDbUri = "postgresql://"
, configDbUseLegacyGucs = True
, configFilePath = Nothing
, configJWKS = parseSecret <$> secret
, configJwtAudience = Nothing
, configJwtRoleClaimKey = [JSPKey "role"]
, configJwtSecret = secret
, configJwtSecretIsBase64 = False
, configJwtCacheMaxLifetime = 0
, configLogLevel = LogCrit
, configOpenApiMode = OAFollowPriv
, configOpenApiSecurityActive = False
, configOpenApiServerProxyUri = Nothing
, configRawMediaTypes = []
, configServerCorsAllowedOrigins = Nothing
, configServerHost = "localhost"
, configServerPort = 3000
, configServerTraceHeader = Nothing
, configServerUnixSocket = Nothing
, configServerUnixSocketMode = 432
, configDbTxAllowOverride = True
, configDbTxRollbackAll = True
, configAdminServerPort = Nothing
, configRoleSettings = mempty
, configRoleIsoLvl = mempty
, configInternalSCSleep = Nothing
}

testCfg :: AppConfig
Expand Down
Loading