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 3, 2023
1 parent af0e369 commit c30e87f
Show file tree
Hide file tree
Showing 17 changed files with 60 additions and 16 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ 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

Expand Down
7 changes: 4 additions & 3 deletions src/PostgREST/App.hs
Original file line number Diff line number Diff line change
Expand Up @@ -216,16 +216,17 @@ handleRequest AuthResult{..} conf appState authenticated prepared pgVer apiReq@A
return $ pgrstResponse pgrst

(ActionInfo, TargetIdent identifier) -> do
pgrst <- liftEither $ Response.infoIdentResponse identifier sCache
pgrst <- liftEither $ Response.infoIdentResponse identifier sCache (configCorsAllowedOrigins conf)
return $ pgrstResponse pgrst

(ActionInfo, TargetProc identifier _) -> do
cPlan <- liftEither $ Plan.callReadPlan identifier conf sCache apiReq ApiRequest.InvHead
pgrst <- liftEither $ Response.infoProcResponse (Plan.crProc cPlan)
pgrst <- liftEither $ Response.infoProcResponse (Plan.crProc cPlan) (configCorsAllowedOrigins conf)

return $ pgrstResponse pgrst

(ActionInfo, TargetDefaultSpec _) -> do
pgrst <- liftEither Response.infoRootResponse
pgrst <- liftEither $ Response.infoRootResponse (configCorsAllowedOrigins conf)
return $ pgrstResponse pgrst

_ ->
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
26 changes: 14 additions & 12 deletions src/PostgREST/Response.hs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import qualified Data.Aeson as JSON
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.HashMap.Strict as HM
import qualified Data.Text.Encoding as T
import Data.Text.Read (decimal)
import qualified Network.HTTP.Types.Header as HTTP
import qualified Network.HTTP.Types.Status as HTTP
Expand Down Expand Up @@ -209,10 +210,10 @@ deleteResponse MutateReadPlan{mrMedia} ctxApiRequest@ApiRequest{iPreferences=Pre
RSPlan plan ->
Right $ PgrstResponse HTTP.status200 (contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan

infoIdentResponse :: QualifiedIdentifier -> SchemaCache -> Either Error.Error PgrstResponse
infoIdentResponse identifier sCache = do
infoIdentResponse :: QualifiedIdentifier -> SchemaCache -> Text -> Either Error.Error PgrstResponse
infoIdentResponse identifier sCache corsAllowedOrigins = do
case HM.lookup identifier (dbTables sCache) of
Just tbl -> respondInfo $ allowH tbl
Just tbl -> respondInfo corsAllowedOrigins $ allowH tbl
Nothing -> Left $ Error.ApiRequestError ApiRequestTypes.NotFound
where
allowH table =
Expand All @@ -224,17 +225,18 @@ infoIdentResponse identifier sCache = do
["PATCH" | tableUpdatable table] ++
["DELETE" | tableDeletable table]

infoProcResponse :: Routine -> Either Error.Error PgrstResponse
infoProcResponse proc | pdVolatility proc == Volatile = respondInfo "OPTIONS,POST"
| otherwise = respondInfo "OPTIONS,GET,HEAD,POST"
infoProcResponse :: Routine -> Text -> Either Error.Error PgrstResponse
infoProcResponse proc corsAllowedOrigins
| pdVolatility proc == Volatile = respondInfo corsAllowedOrigins "OPTIONS,POST"
| otherwise = respondInfo corsAllowedOrigins "OPTIONS,GET,HEAD,POST"

infoRootResponse :: Either Error.Error PgrstResponse
infoRootResponse = respondInfo "OPTIONS,GET,HEAD"
infoRootResponse :: Text -> Either Error.Error PgrstResponse
infoRootResponse corsAllowedOrigins = respondInfo corsAllowedOrigins "OPTIONS,GET,HEAD"

respondInfo :: ByteString -> Either Error.Error PgrstResponse
respondInfo allowHeader =
let allOrigins = ("Access-Control-Allow-Origin", "*") in
Right $ PgrstResponse HTTP.status200 [allOrigins, (HTTP.hAllow, allowHeader)] mempty
respondInfo :: Text -> ByteString -> Either Error.Error PgrstResponse
respondInfo corsAllowedOrigins allowHeader =
let allowedOrigins = ("Access-Control-Allow-Origin", T.encodeUtf8 corsAllowedOrigins) in
Right $ PgrstResponse HTTP.status200 [allowedOrigins, (HTTP.hAllow, allowHeader)] mempty

invokeResponse :: CallReadPlan -> InvokeMethod -> Routine -> ApiRequest -> ResultSet -> Maybe ServerTimingParams -> Either Error.Error PgrstResponse
invokeResponse CallReadPlan{crMedia} invMethod proc ctxApiRequest@ApiRequest{iPreferences=Preferences{..},..} resultSet serverTimingParams = case resultSet of
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
23 changes: 23 additions & 0 deletions test/io/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -1214,3 +1214,26 @@ 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_cors_allowed_origin_config_no_default(defaultenv):
"OPTIONS request should return Access-Control-Allow-Origin with config"

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

headers = {
"Accept": "*/*",
"Origin": "http://example.com",
"Access-Control-Allow-Method": "POST",
"Access-Control-Allow-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, http://example2.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 c30e87f

Please sign in to comment.