Skip to content

Commit a36f949

Browse files
committed
Reload external file on SIGUSR2/NOTIFY
1 parent beea3fe commit a36f949

File tree

5 files changed

+129
-60
lines changed

5 files changed

+129
-60
lines changed

main/Main.hs

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ main = do
7070
pathEnvConf <- either panic identity <$> readAppConfig mempty env cliPath Nothing Nothing
7171

7272
-- read external files
73-
dbUriFile <- readDbUriFile $ configDbUri pathEnvConf
73+
dbUriFile <- readDbUriFile $ configDbUri pathEnvConf
7474
secretFile <- readSecretFile $ configJwtSecret pathEnvConf
7575

7676
-- add the external files to AppConfig
@@ -111,10 +111,19 @@ main = do
111111
-- Config that can change at runtime
112112
refConf <- newIORef conf
113113

114-
let dbConfigReader startingUp = readDbConfig startingUp pool gucConfigEnabled env cliPath dbUriFile secretFile refConf
115-
116-
-- Override the config with config options from the db, only if db-load-guc-config is true
117-
when gucConfigEnabled $ dbConfigReader True
114+
let
115+
-- re-reads config file + db config
116+
dbConfigReReader startingUp = when gucConfigEnabled $
117+
reReadConfig startingUp pool gucConfigEnabled env cliPath refConf dbUriFile secretFile
118+
-- re-reads jwt-secret external file + config file + db config
119+
fullConfigReReader =
120+
reReadConfig False pool gucConfigEnabled env cliPath refConf
121+
dbUriFile =<< -- db-uri external file could be re-read, but it doesn't make sense as db-uri is not reloadable
122+
readSecretFile (configJwtSecret pathEnvConf)
123+
124+
-- Override the config with config options from the db
125+
-- TODO: the same operation is repeated on connectionWorker, ideally this would be done only once, but dump CmdDumpConfig needs it for tests.
126+
dbConfigReReader True
118127

119128
case cliCommand of
120129
CmdDumpConfig ->
@@ -133,7 +142,8 @@ main = do
133142
-- This is passed to the connectionWorker method so it can kill the main thread if the PostgreSQL's version is not supported.
134143
mainTid <- myThreadId
135144

136-
let connWorker = connectionWorker mainTid pool refConf refDbStructure refIsWorkerOn (dbChannelEnabled, mvarConnectionStatus) $ dbConfigReader False
145+
let connWorker = connectionWorker mainTid pool refConf refDbStructure refIsWorkerOn (dbChannelEnabled, mvarConnectionStatus) $
146+
dbConfigReReader False
137147

138148
-- Sets the initial refDbStructure
139149
connWorker
@@ -156,13 +166,13 @@ main = do
156166

157167
-- Re-read the config on SIGUSR2
158168
void $ installHandler sigUSR2 (
159-
Catch $ dbConfigReader False
169+
Catch fullConfigReReader
160170
) Nothing
161171
#endif
162172

163173
-- reload schema cache + config on NOTIFY
164174
when dbChannelEnabled $
165-
listener dbUri dbChannel pool refConf refDbStructure mvarConnectionStatus connWorker $ dbConfigReader False
175+
listener dbUri dbChannel pool refConf refDbStructure mvarConnectionStatus connWorker fullConfigReReader
166176

167177
-- ask for the OS time at most once per second
168178
getTime <- mkAutoUpdate defaultUpdateSettings {updateAction = getCurrentTime}
@@ -219,7 +229,7 @@ connectionWorker
219229
-> (Bool, MVar ConnectionStatus) -- ^ For interacting with the LISTEN channel
220230
-> IO ()
221231
-> IO ()
222-
connectionWorker mainTid pool refConf refDbStructure refIsWorkerOn (dbChannelEnabled, mvarConnectionStatus) cfRereader = do
232+
connectionWorker mainTid pool refConf refDbStructure refIsWorkerOn (dbChannelEnabled, mvarConnectionStatus) dbCfReader = do
223233
isWorkerOn <- readIORef refIsWorkerOn
224234
unless isWorkerOn $ do -- Prevents multiple workers to be running at the same time. Could happen on too many SIGUSR1s.
225235
atomicWriteIORef refIsWorkerOn True
@@ -235,7 +245,7 @@ connectionWorker mainTid pool refConf refDbStructure refIsWorkerOn (dbChannelEna
235245
NotConnected -> return () -- Unreachable because connectionStatus will keep trying to connect
236246
Connected actualPgVersion -> do -- Procede with initialization
237247
putStrLn ("Connection successful" :: Text)
238-
cfRereader
248+
dbCfReader -- this could be fail because the connection drops, but the loadSchemaCache will pick the error and retry again
239249
scStatus <- loadSchemaCache pool actualPgVersion refConf refDbStructure
240250
case scStatus of
241251
SCLoaded -> pure () -- do nothing and proceed if the load was successful
@@ -340,9 +350,9 @@ listener dbUri dbChannel pool refConf refDbStructure mvarConnectionStatus connWo
340350
errorMessage = "Could not listen for notifications on the " <> dbChannel <> " channel" :: Text
341351
retryMessage = "Retrying listening for notifications on the " <> dbChannel <> " channel.." :: Text
342352

343-
-- | Reads the config options from the db
344-
readDbConfig :: Bool -> P.Pool -> Bool -> Environment -> Maybe FilePath -> Maybe Text -> Maybe BS.ByteString -> IORef AppConfig -> IO ()
345-
readDbConfig startingUp pool gucConfigEnabled env path dbUriFile secretFile refConf = do
353+
-- | Re-reads the config plus config options from the db
354+
reReadConfig :: Bool -> P.Pool -> Bool -> Environment -> Maybe FilePath -> IORef AppConfig -> Maybe Text -> Maybe BS.ByteString -> IO ()
355+
reReadConfig startingUp pool gucConfigEnabled env path refConf dbUriFile secretFile = do
346356
dbSettings <- if gucConfigEnabled then loadDbSettings else pure []
347357
readAppConfig dbSettings env path dbUriFile secretFile >>= \case
348358
Left err ->

test/fixtures/roles.sql

Lines changed: 41 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,55 +7,55 @@ CREATE ROLE postgrest_test_author;
77
GRANT postgrest_test_anonymous, postgrest_test_default_role, postgrest_test_author TO :USER;
88

99
-- reloadable config options for io tests
10-
ALTER ROLE postgrest_test_authenticator SET pgrst."jwt_aud" = 'https://example.org';
11-
ALTER ROLE postgrest_test_authenticator SET pgrst."openapi_server_proxy_uri" = 'https://example.org/api';
12-
ALTER ROLE postgrest_test_authenticator SET pgrst."raw_media_types" = 'application/vnd.pgrst.db-config';
13-
ALTER ROLE postgrest_test_authenticator SET pgrst."jwt_secret" = 'REALLYREALLYREALLYREALLYVERYSAFE';
14-
ALTER ROLE postgrest_test_authenticator SET pgrst."jwt_secret_is_base64" = 'true';
15-
ALTER ROLE postgrest_test_authenticator SET pgrst."jwt_role_claim_key" = '."a"."role"';
16-
ALTER ROLE postgrest_test_authenticator SET pgrst."db_tx_end" = 'commit-allow-override';
17-
ALTER ROLE postgrest_test_authenticator SET pgrst."db_schemas" = 'test, tenant1, tenant2';
18-
ALTER ROLE postgrest_test_authenticator SET pgrst."db_root_spec" = 'root';
19-
ALTER ROLE postgrest_test_authenticator SET pgrst."db_prepared_statements" = 'false';
20-
ALTER ROLE postgrest_test_authenticator SET pgrst."db_pre_request" = 'test.custom_headers';
21-
ALTER ROLE postgrest_test_authenticator SET pgrst."db_max_rows" = '1000';
22-
ALTER ROLE postgrest_test_authenticator SET pgrst."db_extra_search_path" = 'public, extensions';
10+
ALTER ROLE postgrest_test_authenticator SET pgrst.jwt_aud = 'https://example.org';
11+
ALTER ROLE postgrest_test_authenticator SET pgrst.openapi_server_proxy_uri = 'https://example.org/api';
12+
ALTER ROLE postgrest_test_authenticator SET pgrst.raw_media_types = 'application/vnd.pgrst.db-config';
13+
ALTER ROLE postgrest_test_authenticator SET pgrst.jwt_secret = 'REALLYREALLYREALLYREALLYVERYSAFE';
14+
ALTER ROLE postgrest_test_authenticator SET pgrst.jwt_secret_is_base64 = 'true';
15+
ALTER ROLE postgrest_test_authenticator SET pgrst.jwt_role_claim_key = '."a"."role"';
16+
ALTER ROLE postgrest_test_authenticator SET pgrst.db_tx_end = 'commit-allow-override';
17+
ALTER ROLE postgrest_test_authenticator SET pgrst.db_schemas = 'test, tenant1, tenant2';
18+
ALTER ROLE postgrest_test_authenticator SET pgrst.db_root_spec = 'root';
19+
ALTER ROLE postgrest_test_authenticator SET pgrst.db_prepared_statements = 'false';
20+
ALTER ROLE postgrest_test_authenticator SET pgrst.db_pre_request = 'test.custom_headers';
21+
ALTER ROLE postgrest_test_authenticator SET pgrst.db_max_rows = '1000';
22+
ALTER ROLE postgrest_test_authenticator SET pgrst.db_extra_search_path = 'public, extensions';
2323

2424
-- override with database specific setting
25-
ALTER ROLE postgrest_test_authenticator IN DATABASE :DBNAME SET pgrst."jwt_secret" = 'OVERRIDEREALLYREALLYREALLYREALLYVERYSAFE';
26-
ALTER ROLE postgrest_test_authenticator IN DATABASE :DBNAME SET pgrst."db_extra_search_path" = 'public, extensions, private';
25+
ALTER ROLE postgrest_test_authenticator IN DATABASE :DBNAME SET pgrst.jwt_secret = 'OVERRIDEREALLYREALLYREALLYREALLYVERYSAFE';
26+
ALTER ROLE postgrest_test_authenticator IN DATABASE :DBNAME SET pgrst.db_extra_search_path = 'public, extensions, private';
2727

2828
-- other database settings that should be ignored
2929
DROP DATABASE IF EXISTS other;
3030
CREATE DATABASE other;
31-
ALTER ROLE postgrest_test_authenticator IN DATABASE other SET pgrst."db_max_rows" = '1111';
31+
ALTER ROLE postgrest_test_authenticator IN DATABASE other SET pgrst.db_max_rows = '1111';
3232

3333
-- non-reloadable configs for io tests
34-
ALTER ROLE postgrest_test_authenticator SET pgrst."server_host" = 'ignored';
35-
ALTER ROLE postgrest_test_authenticator SET pgrst."server_port" = 'ignored';
36-
ALTER ROLE postgrest_test_authenticator SET pgrst."server_unix_socket" = 'ignored';
37-
ALTER ROLE postgrest_test_authenticator SET pgrst."server_unix_socket_mode" = 'ignored';
38-
ALTER ROLE postgrest_test_authenticator SET pgrst."log_level" = 'ignored';
39-
ALTER ROLE postgrest_test_authenticator SET pgrst."db_anon_role" = 'ignored';
40-
ALTER ROLE postgrest_test_authenticator SET pgrst."db_uri" = 'postgresql://ignored';
41-
ALTER ROLE postgrest_test_authenticator SET pgrst."db_channel_enabled" = 'ignored';
42-
ALTER ROLE postgrest_test_authenticator SET pgrst."db_channel" = 'ignored';
43-
ALTER ROLE postgrest_test_authenticator SET pgrst."db_pool" = 'ignored';
44-
ALTER ROLE postgrest_test_authenticator SET pgrst."db_pool_timeout" = 'ignored';
45-
ALTER ROLE postgrest_test_authenticator SET pgrst."db_load_guc_config" = 'ignored';
34+
ALTER ROLE postgrest_test_authenticator SET pgrst.server_host = 'ignored';
35+
ALTER ROLE postgrest_test_authenticator SET pgrst.server_port = 'ignored';
36+
ALTER ROLE postgrest_test_authenticator SET pgrst.server_unix_socket = 'ignored';
37+
ALTER ROLE postgrest_test_authenticator SET pgrst.server_unix_socket_mode = 'ignored';
38+
ALTER ROLE postgrest_test_authenticator SET pgrst.log_level = 'ignored';
39+
ALTER ROLE postgrest_test_authenticator SET pgrst.db_anon_role = 'ignored';
40+
ALTER ROLE postgrest_test_authenticator SET pgrst.db_uri = 'postgresql://ignored';
41+
ALTER ROLE postgrest_test_authenticator SET pgrst.db_channel_enabled = 'ignored';
42+
ALTER ROLE postgrest_test_authenticator SET pgrst.db_channel = 'ignored';
43+
ALTER ROLE postgrest_test_authenticator SET pgrst.db_pool = 'ignored';
44+
ALTER ROLE postgrest_test_authenticator SET pgrst.db_pool_timeout = 'ignored';
45+
ALTER ROLE postgrest_test_authenticator SET pgrst.db_load_guc_config = 'ignored';
4646

4747
-- other authenticator reloadable config options for io tests
4848
CREATE ROLE other_authenticator LOGIN NOINHERIT;
49-
ALTER ROLE other_authenticator SET pgrst."jwt_aud" = 'https://otherexample.org';
50-
ALTER ROLE other_authenticator SET pgrst."openapi_server_proxy_uri" = 'https://otherexample.org/api';
51-
ALTER ROLE other_authenticator SET pgrst."raw_media_types" = 'application/vnd.pgrst.other-db-config';
52-
ALTER ROLE other_authenticator SET pgrst."jwt_secret" = 'ODERREALLYREALLYREALLYREALLYVERYSAFE';
53-
ALTER ROLE other_authenticator SET pgrst."jwt_secret_is_base64" = 'true';
54-
ALTER ROLE other_authenticator SET pgrst."jwt_role_claim_key" = '."other"."role"';
55-
ALTER ROLE other_authenticator SET pgrst."db_tx_end" = 'rollback-allow-override';
56-
ALTER ROLE other_authenticator SET pgrst."db_schemas" = 'test, other_tenant1, other_tenant2';
57-
ALTER ROLE other_authenticator SET pgrst."db_root_spec" = 'other_root';
58-
ALTER ROLE other_authenticator SET pgrst."db_prepared_statements" = 'false';
59-
ALTER ROLE other_authenticator SET pgrst."db_pre_request" = 'test.other_custom_headers';
60-
ALTER ROLE other_authenticator SET pgrst."db_max_rows" = '100';
61-
ALTER ROLE other_authenticator SET pgrst."db_extra_search_path" = 'public, extensions, other';
49+
ALTER ROLE other_authenticator SET pgrst.jwt_aud = 'https://otherexample.org';
50+
ALTER ROLE other_authenticator SET pgrst.openapi_server_proxy_uri = 'https://otherexample.org/api';
51+
ALTER ROLE other_authenticator SET pgrst.raw_media_types = 'application/vnd.pgrst.other-db-config';
52+
ALTER ROLE other_authenticator SET pgrst.jwt_secret = 'ODERREALLYREALLYREALLYREALLYVERYSAFE';
53+
ALTER ROLE other_authenticator SET pgrst.jwt_secret_is_base64 = 'true';
54+
ALTER ROLE other_authenticator SET pgrst.jwt_role_claim_key = '."other"."role"';
55+
ALTER ROLE other_authenticator SET pgrst.db_tx_end = 'rollback-allow-override';
56+
ALTER ROLE other_authenticator SET pgrst.db_schemas = 'test, other_tenant1, other_tenant2';
57+
ALTER ROLE other_authenticator SET pgrst.db_root_spec = 'other_root';
58+
ALTER ROLE other_authenticator SET pgrst.db_prepared_statements = 'false';
59+
ALTER ROLE other_authenticator SET pgrst.db_pre_request = 'test.other_custom_headers';
60+
ALTER ROLE other_authenticator SET pgrst.db_max_rows = '100';
61+
ALTER ROLE other_authenticator SET pgrst.db_extra_search_path = 'public, extensions, other';

test/fixtures/schema.sql

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1928,7 +1928,7 @@ select * from pg_catalog.pg_prepared_statements;
19281928
create or replace function change_max_rows_config(val int, notify bool default false) returns void as $_$
19291929
begin
19301930
execute format($$
1931-
alter role postgrest_test_authenticator set pgrst."db_max_rows" = %L;
1931+
alter role postgrest_test_authenticator set pgrst.db_max_rows = %L;
19321932
$$, val);
19331933
if notify then
19341934
perform pg_notify('pgrst', 'reload config');
@@ -1937,33 +1937,39 @@ end $_$ volatile security definer language plpgsql ;
19371937

19381938
create or replace function reset_max_rows_config() returns void as $_$
19391939
begin
1940-
alter role postgrest_test_authenticator set pgrst."db_max_rows" = '1000';
1940+
alter role postgrest_test_authenticator set pgrst.db_max_rows = '1000';
19411941
end $_$ volatile security definer language plpgsql ;
19421942

19431943
create or replace function change_db_schema_and_full_reload(schemas text) returns void as $_$
19441944
begin
19451945
execute format($$
1946-
alter role postgrest_test_authenticator set pgrst."db_schemas" = %L;
1946+
alter role postgrest_test_authenticator set pgrst.db_schemas = %L;
19471947
$$, schemas);
19481948
perform pg_notify('pgrst', 'reload config');
19491949
perform pg_notify('pgrst', 'reload schema');
19501950
end $_$ volatile security definer language plpgsql ;
19511951

19521952
create or replace function v1.reset_db_schema_config() returns void as $_$
19531953
begin
1954-
alter role postgrest_test_authenticator set pgrst."db_schemas" = 'test';
1954+
alter role postgrest_test_authenticator set pgrst.db_schemas = 'test';
19551955
perform pg_notify('pgrst', 'reload config');
19561956
perform pg_notify('pgrst', 'reload schema');
19571957
end $_$ volatile security definer language plpgsql ;
19581958

19591959
create or replace function test.invalid_role_claim_key_reload() returns void as $_$
19601960
begin
1961-
alter role postgrest_test_authenticator set pgrst."jwt_role_claim_key" = 'test';
1961+
alter role postgrest_test_authenticator set pgrst.jwt_role_claim_key = 'test';
19621962
perform pg_notify('pgrst', 'reload config');
19631963
end $_$ volatile security definer language plpgsql ;
19641964

19651965
create or replace function test.reset_invalid_role_claim_key() returns void as $_$
19661966
begin
1967-
alter role postgrest_test_authenticator set pgrst."jwt_role_claim_key" = '."a"."role"';
1967+
alter role postgrest_test_authenticator set pgrst.jwt_role_claim_key = '."a"."role"';
19681968
perform pg_notify('pgrst', 'reload config');
19691969
end $_$ volatile security definer language plpgsql ;
1970+
1971+
create or replace function test.reload_pgrst_config() returns void as $_$
1972+
begin
1973+
alter role postgrest_test_authenticator set pgrst.jwt_role_claim_key = '."a"."role"';
1974+
perform pg_notify('pgrst', 'reload config');
1975+
end $_$ language plpgsql ;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
db-pool = 1
2+
3+
jwt-secret = "$(JWT_SECRET_FILE)"
4+
jwt-secret-is-base64 = false
5+
db-load-guc-config = false

test/io-tests/test_io.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ def test_stable_config(tmp_path, config, defaultenv):
336336
"ROLE_CLAIM_KEY": '."https://www.example.com/roles"[0].value',
337337
"POSTGREST_TEST_SOCKET": "/tmp/postgrest.sock",
338338
"POSTGREST_TEST_PORT": "80",
339+
"JWT_SECRET_FILE": "a_file",
339340
}
340341

341342
# Some configs expect input from stdin, at least on base64.
@@ -508,6 +509,53 @@ def test_jwt_secret_reload(tmp_path, defaultenv):
508509
assert response.status_code == 200
509510

510511

512+
def test_jwt_secret_external_file_reload(tmp_path, defaultenv):
513+
"JWT secret external file should be reloaded when PostgREST is sent a SIGUSR2 or a NOTIFY."
514+
config = CONFIGSDIR / "sigusr2-settings-external-secret.config"
515+
516+
headers = jwtauthheader({"role": "postgrest_test_author"}, SECRET)
517+
518+
external_secret_file = tmp_path / "jwt-secret-config"
519+
external_secret_file.write_text("invalid" * 5)
520+
521+
env = {
522+
**defaultenv,
523+
"JWT_SECRET_FILE": f"@{external_secret_file}",
524+
"PGRST_DB_CHANNEL_ENABLED": "true",
525+
}
526+
527+
with run(config, env=env) as postgrest:
528+
response = postgrest.session.get("/authors_only", headers=headers)
529+
assert response.status_code == 401
530+
531+
# change external file
532+
external_secret_file.write_text(SECRET)
533+
534+
# SIGUSR1 doesn't reload external files
535+
postgrest.process.send_signal(signal.SIGUSR1)
536+
time.sleep(0.1)
537+
538+
response = postgrest.session.get("/authors_only", headers=headers)
539+
assert response.status_code == 401
540+
541+
# reload config and external file with SIGUSR2
542+
postgrest.process.send_signal(signal.SIGUSR2)
543+
time.sleep(0.1)
544+
545+
response = postgrest.session.get("/authors_only", headers=headers)
546+
assert response.status_code == 200
547+
548+
# change external file to wrong value again
549+
external_secret_file.write_text("invalid" * 5)
550+
551+
# reload config and external file with NOTIFY
552+
postgrest.session.get("/rpc/reload_pgrst_config")
553+
time.sleep(0.1)
554+
555+
response = postgrest.session.get("/authors_only", headers=headers)
556+
assert response.status_code == 401
557+
558+
511559
def test_db_schema_reload(tmp_path, defaultenv):
512560
"DB schema should be reloaded when PostgREST is sent SIGUSR2."
513561
config = (CONFIGSDIR / "sigusr2-settings.config").read_text()

0 commit comments

Comments
 (0)