Skip to content

Commit

Permalink
feat: add config to specify CORS origins
Browse files Browse the repository at this point in the history
  • Loading branch information
taimoorzaeem committed Oct 10, 2023
1 parent 056c748 commit f3c603a
Show file tree
Hide file tree
Showing 17 changed files with 77 additions and 7 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #2771, Add `Server-Timing` header with JWT duration - @taimoorzaeem
- #2698, Add config `jwt-cache-max-lifetime` and implement JWT caching - @taimoorzaeem
- #2943, Add `handling=strict/lenient` for Prefer header - @taimoorzaeem
- #2441, Add config `cors-allowed-origins` to specify CORS origins - @taimoorzaeem

### Fixed

- #2824, Fix range request with 0 rows and 0 offset return status 416 - @strengthless

## [11.2.1] - 2023-10-03
Expand Down
2 changes: 1 addition & 1 deletion src/PostgREST/App.hs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ serverSettings AppConfig{..} =
postgrest :: AppConfig -> AppState.AppState -> IO () -> Wai.Application
postgrest conf appState connWorker =
traceHeaderMiddleware conf .
Cors.middleware .
Cors.middleware (configCorsAllowedOrigins 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 @@ -126,6 +126,9 @@ exampleConfigFile =
[str|## Admin server used for checks. It's disabled by default unless a port is specified.
|# admin-server-port = 3001
|
|## Configurable CORS origins
|# cors-allowed-origins = "*"
|
|## The database role to use when no client authentication is provided
|# db-anon-role = "anon"
|
Expand Down
5 changes: 4 additions & 1 deletion src/PostgREST/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import Protolude hiding (Proxy, toList)

data AppConfig = AppConfig
{ configAppSettings :: [(Text, Text)]
, configCorsAllowedOrigins :: Text
, configDbAnonRole :: Maybe BS.ByteString
, configDbChannel :: Text
, configDbChannelEnabled :: Bool
Expand Down Expand Up @@ -139,7 +140,8 @@ toText conf =
where
-- apply conf to all pgrst settings
pgrstSettings = (\(k, v) -> (k, v conf)) <$>
[("db-anon-role", q . T.decodeUtf8 . fromMaybe "" . configDbAnonRole)
[("cors-allowed-origins", q . configCorsAllowedOrigins)
,("db-anon-role", q . T.decodeUtf8 . fromMaybe "" . configDbAnonRole)
,("db-channel", q . configDbChannel)
,("db-channel-enabled", T.toLower . show . configDbChannelEnabled)
,("db-extra-search-path", q . T.intercalate "," . configDbExtraSearchPath)
Expand Down Expand Up @@ -233,6 +235,7 @@ parser :: Maybe FilePath -> Environment -> [(Text, Text)] -> RoleSettings -> Rol
parser optPath env dbSettings roleSettings roleIsolationLvl =
AppConfig
<$> parseAppSettings "app.settings"
<*> (fromMaybe "*" <$> optString "cors-allowed-origins")
<*> (fmap encodeUtf8 <$> optString "db-anon-role")
<*> (fromMaybe "pgrst" <$> optString "db-channel")
<*> (fromMaybe True <$> optBool "db-channel-enabled")
Expand Down
15 changes: 10 additions & 5 deletions src/PostgREST/Cors.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,24 @@ module PostgREST.Cors (middleware) where

import qualified Data.ByteString.Char8 as BS
import qualified Data.CaseInsensitive as CI
import qualified Data.Text as T
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 :: 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
corsPolicy :: Text -> Wai.Request -> Maybe Wai.CorsResourcePolicy
corsPolicy corsAllowedOrigins req = case lookup "origin" headers of
Just origin ->
Just Wai.CorsResourcePolicy
{ Wai.corsOrigins = Just ([origin], True)
{ Wai.corsOrigins = if checkOrigin origin then Just ([origin],True) else Nothing
, Wai.corsMethods = ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"]
, Wai.corsRequestHeaders = "Authorization" : accHeaders
, Wai.corsExposedHeaders = Just
Expand All @@ -34,6 +36,9 @@ corsPolicy req = case lookup "origin" headers of
}
Nothing -> Nothing
where
checkOrigin origin =
corsAllowedOrigins == "*" ||
T.decodeUtf8 origin `elem` T.splitOn ", " corsAllowedOrigins
headers = Wai.requestHeaders req
accHeaders = case lookup "access-control-request-headers" headers of
Just hdrs -> map (CI.mk . BS.strip) $ BS.split ',' hdrs
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/aliases.config
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
cors-allowed-origins = "*"
db-anon-role = ""
db-channel = "pgrst"
db-channel-enabled = true
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
@@ -1,3 +1,4 @@
cors-allowed-origins = "*"
db-anon-role = ""
db-channel = "pgrst"
db-channel-enabled = true
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
@@ -1,3 +1,4 @@
cors-allowed-origins = "*"
db-anon-role = ""
db-channel = "pgrst"
db-channel-enabled = true
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/defaults.config
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
cors-allowed-origins = "*"
db-anon-role = ""
db-channel = "pgrst"
db-channel-enabled = true
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
cors-allowed-origins = "http://example.com"
db-anon-role = "pre_config_role"
db-channel = "postgrest"
db-channel-enabled = false
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
@@ -1,3 +1,4 @@
cors-allowed-origins = "http://example.com"
db-anon-role = "anonymous"
db-channel = "postgrest"
db-channel-enabled = false
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
@@ -1,3 +1,4 @@
cors-allowed-origins = "http://example.com"
db-anon-role = "root"
db-channel = "postgrest"
db-channel-enabled = false
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/types.config
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
cors-allowed-origins = "*"
db-anon-role = ""
db-channel = "pgrst"
db-channel-enabled = true
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
@@ -1,5 +1,6 @@
PGRST_APP_SETTINGS_test2: test
PGRST_APP_SETTINGS_test: test
PGRST_CORS_ALLOWED_ORIGINS: "http://example.com"
PGRST_DB_ANON_ROLE: root
PGRST_DB_CHANNEL: postgrest
PGRST_DB_CHANNEL_ENABLED: false
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/no-defaults.config
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
cors-allowed-origins = "http://example.com"
db-anon-role = "root"
db-channel = "postgrest"
db-channel-enabled = false
Expand Down
44 changes: 44 additions & 0 deletions test/io/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -1214,3 +1214,47 @@ 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_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_options_no_preflight_request_with_cors_allowed_origin_config(defaultenv):
"OPTIONS no preflight request should return Access-Control-Allow-Origin equal to origin"

env = {
**defaultenv,
"PGRST_CORS_ALLOWED_ORIGINS": "http://example.com, http://example2.com",
}

headers = {
"Accept": "*/*",
"Origin": "http://example.com",
}

with run(env=env) as postgrest:
response = postgrest.session.options("/items", headers=headers)
assert (
response.headers["Access-Control-Allow-Origin"]
== "*" # not sure, maybe "http://example.com" ?
)
1 change: 1 addition & 0 deletions test/spec/SpecHelper.hs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ baseCfg :: AppConfig
baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
AppConfig {
configAppSettings = [ ("app.settings.app_host", "localhost") , ("app.settings.external_api_secret", "0123456789abcdef") ]
, configCorsAllowedOrigins = "*"
, configDbAnonRole = Just "postgrest_test_anonymous"
, configDbChannel = mempty
, configDbChannelEnabled = True
Expand Down

0 comments on commit f3c603a

Please sign in to comment.