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 23, 2023
1 parent 618f93d commit 3fa6ab5
Show file tree
Hide file tree
Showing 18 changed files with 90 additions and 10 deletions.
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 `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 (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 :: Maybe [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 . maybe "" (T.intercalate ",") . 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"
<*> (fmap splitOnCommas <$> optValue "cors-allowed-origins")
<*> (fmap encodeUtf8 <$> optString "db-anon-role")
<*> (fromMaybe "pgrst" <$> optString "db-channel")
<*> (fromMaybe True <$> optBool "db-channel-enabled")
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
@@ -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
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_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_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_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
1 change: 1 addition & 0 deletions test/spec/SpecHelper.hs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ baseCfg :: AppConfig
baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
AppConfig {
configAppSettings = [ ("app.settings.app_host", "localhost") , ("app.settings.external_api_secret", "0123456789abcdef") ]
, configCorsAllowedOrigins = Nothing
, configDbAnonRole = Just "postgrest_test_anonymous"
, configDbChannel = mempty
, configDbChannelEnabled = True
Expand Down

0 comments on commit 3fa6ab5

Please sign in to comment.