Skip to content
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
3 changes: 2 additions & 1 deletion database/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ ALTER TABLE file_processing_log FORCE ROW LEVEL SECURITY;
CREATE POLICY user_isolation ON file_processing_log
USING (user_id = current_user);

GRANT SELECT ON file_processing_log TO demo_openmrg, demo_orange_cameroun, webserver_role;
GRANT SELECT, INSERT ON file_processing_log TO demo_openmrg, demo_orange_cameroun;
GRANT SELECT ON file_processing_log TO webserver_role;
GRANT USAGE ON SEQUENCE file_processing_log_id_seq TO demo_openmrg, demo_orange_cameroun;
GRANT SELECT ON cml_data_1h_secure TO webserver_role;
17 changes: 8 additions & 9 deletions database/migrations/008_add_file_processing_log.sql
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,18 @@ CREATE INDEX IF NOT EXISTS file_processing_log_status_time_idx
ON file_processing_log (status, processed_at DESC);

-- Row-Level Security: each login role only sees its own rows (user_id = current_user).
-- myuser (superuser) bypasses RLS so the parser continues to INSERT without restriction.
-- The parser connects as the per-user role, so RLS is enforced; INSERT is permitted
-- because the user_id column must equal current_user (guaranteed by the parser).
ALTER TABLE file_processing_log ENABLE ROW LEVEL SECURITY;
ALTER TABLE file_processing_log FORCE ROW LEVEL SECURITY;
CREATE POLICY user_isolation ON file_processing_log
USING (user_id = current_user);

-- Grant read access to all user roles so Grafana dashboards can query this table.
-- Each role connects to Postgres as their own login (demo_openmrg, demo_orange_cameroun)
-- and Grafana reads the log for that org's datasource.
-- webserver_role needs it for any admin views.
GRANT SELECT ON file_processing_log TO demo_openmrg, demo_orange_cameroun, webserver_role;
-- Grant read+write access to user roles: the parser connects as the per-user
-- role (e.g. demo_openmrg) and INSERTs a log entry for every processed file.
-- webserver_role only needs SELECT (read-only admin/dashboard view).
GRANT SELECT, INSERT ON file_processing_log TO demo_openmrg, demo_orange_cameroun;
GRANT SELECT ON file_processing_log TO webserver_role;

-- Sequence used by the BIGSERIAL primary key: needed for INSERTs by the parser
-- (which connects as myuser/superuser, so this is a no-op in practice,
-- but keeps the intent explicit for future role changes).
-- Sequence used by the BIGSERIAL primary key: required for INSERT by user roles.
GRANT USAGE ON SEQUENCE file_processing_log_id_seq TO demo_openmrg, demo_orange_cameroun;
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ services:
- SFTP_USERNAME=demo_orange_cameroun
- SFTP_REMOTE_PATH=/uploads/douala
- SFTP_USE_SSH_KEY=true
- SFTP_PRIVATE_KEY_PATH=/app/ssh_keys/id_rsa_demo_orange_cameroun
- SFTP_PRIVATE_KEY_PATH=/app/ssh_keys/id_rsa_orange_cameroun
- SFTP_KNOWN_HOSTS_PATH=/app/ssh_keys/known_hosts
- NETCDF_RSL_VAR=rsl_min
- NETCDF_TSL_VAR=tsl_min
Expand Down
25 changes: 5 additions & 20 deletions grafana/provisioning/datasources/postgres.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,8 @@ datasources:
isDefault: true
editable: false

# Org 2 — demo_orange_cameroun
- name: PostgreSQL
uid: ds_demo_orange_cameroun
type: grafana-postgresql-datasource
access: proxy
orgId: 2
url: database:5432
database: mydatabase
user: demo_orange_cameroun
secureJsonData:
password: demo_orange_cameroun_password
jsonData:
sslmode: disable
isDefault: true
editable: false

# Note: provisioning files only apply to Grafana organisations that
# already exist when Grafana starts. Org 1 (the default) always
# exists. Additional orgs are created by the init_grafana service
# (grafana/init_grafana.py) before it triggers a provisioning reload.
# Note: only org 1 is listed above, intentionally. Grafana reads this
# file at startup, before init_grafana.py has created orgs 2+. Listing
# a non-existent org here causes Grafana to exit with 'org.notFound'.
# Datasources for additional orgs are registered by init_grafana.py via
# the Grafana API after those orgs have been created.
21 changes: 16 additions & 5 deletions scripts/generate_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,11 @@ def generate_users_json(users: list[dict], existing_json: dict) -> dict:
GRANT SELECT, INSERT, UPDATE ON cml_data_secure TO {user_id};
GRANT SELECT ON cml_data_1h_secure TO {user_id};

-- file_processing_log: parser INSERTs a row for every processed file;
-- webserver_role only needs SELECT.
GRANT SELECT, INSERT ON file_processing_log TO {user_id};
GRANT USAGE ON SEQUENCE file_processing_log_id_seq TO {user_id};

-- ---------------------------------------------------------------------------
-- Step 4: Allow webserver_role to impersonate this user
-- ---------------------------------------------------------------------------
Expand Down Expand Up @@ -370,7 +375,12 @@ def generate_grafana_datasources(users: list[dict]) -> str:
" # the correct one — no user interaction required.",
"",
]
for u in users:
# Only org 1 (the Grafana default org) is provisioned here. Grafana reads
# this file at startup, before init_grafana.py has had a chance to create
# orgs 2+. Provisioning a non-existent org causes Grafana to exit with
# "org.notFound". Additional orgs are created by init_grafana.py and their
# datasources are registered there via the Grafana API.
for u in users[:1]:
uid = u["id"]
org_id = u["grafana_org_id"]
lines += [
Expand All @@ -392,10 +402,11 @@ def generate_grafana_datasources(users: list[dict]) -> str:
"",
]
lines += [
"# Note: provisioning files only apply to Grafana organisations that",
"# already exist when Grafana starts. Org 1 (the default) always",
"# exists. Additional orgs are created by the init_grafana service",
"# (grafana/init_grafana.py) before it triggers a provisioning reload.",
"# Note: only org 1 is listed above, intentionally. Grafana reads this",
"# file at startup, before init_grafana.py has created orgs 2+. Listing",
"# a non-existent org here causes Grafana to exit with 'org.notFound'.",
"# Datasources for additional orgs are registered by init_grafana.py via",
"# the Grafana API after those orgs have been created.",
]
return "\n".join(lines) + "\n"

Expand Down
Loading