From ff1f1b06d950e4c9a79400ef671ff1b6ed1051a0 Mon Sep 17 00:00:00 2001 From: Itz-Agasta Date: Tue, 27 Jan 2026 17:49:51 +0530 Subject: [PATCH 1/6] Moves db grant statements to dedicated script Centralizes all read-only access grants into a single SQL script, ensuring permissions are managed in one place. --- lib-sql/grants.sql | 50 +++++++++++++++++++++ lib-sql/tables.sql | 18 -------- lib-sql/tiger_import_finish.sql | 2 - src/nominatim_db/tokenizer/icu_tokenizer.py | 6 --- src/nominatim_db/tools/migration.py | 3 -- 5 files changed, 50 insertions(+), 29 deletions(-) create mode 100644 lib-sql/grants.sql diff --git a/lib-sql/grants.sql b/lib-sql/grants.sql new file mode 100644 index 00000000..6e26eaa8 --- /dev/null +++ b/lib-sql/grants.sql @@ -0,0 +1,50 @@ +-- SPDX-License-Identifier: GPL-2.0-only +-- +-- This file is part of Nominatim. (https://nominatim.org) +-- +-- Copyright (C) 2026 by the Nominatim developer community. +-- For a full list of authors see the git log. +-- +-- Grant read-only access to the web user for all Nominatim tables. + +-- Core tables +GRANT SELECT ON import_status TO "{{config.DATABASE_WEBUSER}}"; +GRANT SELECT ON country_name TO "{{config.DATABASE_WEBUSER}}"; +GRANT SELECT ON nominatim_properties TO "{{config.DATABASE_WEBUSER}}"; + +-- Location tables +GRANT SELECT ON location_property_tiger TO "{{config.DATABASE_WEBUSER}}"; +GRANT SELECT ON location_property_osmline TO "{{config.DATABASE_WEBUSER}}"; +GRANT SELECT ON location_area TO "{{config.DATABASE_WEBUSER}}"; +GRANT SELECT ON location_postcodes TO "{{config.DATABASE_WEBUSER}}"; + +-- Search tables +{% if not db.reverse_only %} +GRANT SELECT ON search_name TO "{{config.DATABASE_WEBUSER}}"; +{% endif %} + +-- Main place tables +GRANT SELECT ON placex TO "{{config.DATABASE_WEBUSER}}"; +GRANT SELECT ON place_addressline TO "{{config.DATABASE_WEBUSER}}"; +GRANT SELECT ON placex_entrance TO "{{config.DATABASE_WEBUSER}}"; + +-- OSM data tables +GRANT SELECT ON planet_osm_ways TO "{{config.DATABASE_WEBUSER}}"; +GRANT SELECT ON planet_osm_rels TO "{{config.DATABASE_WEBUSER}}"; + +-- Error/delete tracking tables +GRANT SELECT ON import_polygon_error TO "{{config.DATABASE_WEBUSER}}"; +GRANT SELECT ON import_polygon_delete TO "{{config.DATABASE_WEBUSER}}"; + +-- Country grid +GRANT SELECT ON country_osm_grid TO "{{config.DATABASE_WEBUSER}}"; + +-- Tokenizer tables (word table) +{% if 'word' in db.tables %} +GRANT SELECT ON word TO "{{config.DATABASE_WEBUSER}}"; +{% endif %} + +-- Tiger import table (if exists) +{% if 'location_property_tiger_import' in db.tables %} +GRANT SELECT ON location_property_tiger_import TO "{{config.DATABASE_WEBUSER}}"; +{% endif %} \ No newline at end of file diff --git a/lib-sql/tables.sql b/lib-sql/tables.sql index c7e301d5..64545b27 100644 --- a/lib-sql/tables.sql +++ b/lib-sql/tables.sql @@ -11,7 +11,6 @@ CREATE TABLE import_status ( sequence_id integer, indexed boolean ); -GRANT SELECT ON import_status TO "{{config.DATABASE_WEBUSER}}" ; drop table if exists import_osmosis_log; CREATE TABLE import_osmosis_log ( @@ -23,14 +22,11 @@ CREATE TABLE import_osmosis_log ( event text ); -GRANT SELECT ON TABLE country_name TO "{{config.DATABASE_WEBUSER}}"; - DROP TABLE IF EXISTS nominatim_properties; CREATE TABLE nominatim_properties ( property TEXT NOT NULL, value TEXT ); -GRANT SELECT ON TABLE nominatim_properties TO "{{config.DATABASE_WEBUSER}}"; drop table IF EXISTS location_area CASCADE; CREATE TABLE location_area ( @@ -66,7 +62,6 @@ CREATE TABLE location_property_tiger ( partition SMALLINT NOT NULL, linegeo GEOMETRY NOT NULL, postcode TEXT); -GRANT SELECT ON location_property_tiger TO "{{config.DATABASE_WEBUSER}}"; drop table if exists location_property_osmline; CREATE TABLE location_property_osmline ( @@ -90,7 +85,6 @@ CREATE UNIQUE INDEX idx_osmline_place_id ON location_property_osmline USING BTRE CREATE INDEX idx_osmline_geometry_sector ON location_property_osmline USING BTREE (geometry_sector) {{db.tablespace.address_index}}; CREATE INDEX idx_osmline_linegeo ON location_property_osmline USING GIST (linegeo) {{db.tablespace.search_index}} WHERE startnumber is not null; -GRANT SELECT ON location_property_osmline TO "{{config.DATABASE_WEBUSER}}"; drop table IF EXISTS search_name; {% if not db.reverse_only %} @@ -106,7 +100,6 @@ CREATE TABLE search_name ( ) {{db.tablespace.search_data}}; CREATE UNIQUE INDEX idx_search_name_place_id ON search_name USING BTREE (place_id) {{db.tablespace.search_index}}; -GRANT SELECT ON search_name to "{{config.DATABASE_WEBUSER}}" ; {% endif %} drop table IF EXISTS place_addressline; @@ -203,11 +196,6 @@ CREATE INDEX idx_placex_rank_boundaries_sector ON placex DROP SEQUENCE IF EXISTS seq_place; CREATE SEQUENCE seq_place start 1; -GRANT SELECT on placex to "{{config.DATABASE_WEBUSER}}" ; -GRANT SELECT on place_addressline to "{{config.DATABASE_WEBUSER}}" ; -GRANT SELECT ON planet_osm_ways to "{{config.DATABASE_WEBUSER}}" ; -GRANT SELECT ON planet_osm_rels to "{{config.DATABASE_WEBUSER}}" ; -GRANT SELECT on location_area to "{{config.DATABASE_WEBUSER}}" ; -- Table for synthetic postcodes. DROP TABLE IF EXISTS location_postcodes; @@ -232,7 +220,6 @@ CREATE INDEX IF NOT EXISTS idx_location_postcodes_postcode {{db.tablespace.search_index}}; CREATE INDEX IF NOT EXISTS idx_location_postcodes_osmid ON location_postcodes USING BTREE (osm_id) {{db.tablespace.search_index}}; -GRANT SELECT ON location_postcodes TO "{{config.DATABASE_WEBUSER}}" ; -- Table to store location of entrance nodes DROP TABLE IF EXISTS placex_entrance; @@ -245,7 +232,6 @@ CREATE TABLE placex_entrance ( ); CREATE UNIQUE INDEX idx_placex_entrance_place_id_osm_id ON placex_entrance USING BTREE (place_id, osm_id) {{db.tablespace.search_index}}; -GRANT SELECT ON placex_entrance TO "{{config.DATABASE_WEBUSER}}" ; -- Create an index on the place table for lookups to populate the entrance -- table @@ -267,7 +253,6 @@ CREATE TABLE import_polygon_error ( newgeometry GEOMETRY(Geometry, 4326) ); CREATE INDEX idx_import_polygon_error_osmid ON import_polygon_error USING BTREE (osm_type, osm_id); -GRANT SELECT ON import_polygon_error TO "{{config.DATABASE_WEBUSER}}"; DROP TABLE IF EXISTS import_polygon_delete; CREATE TABLE import_polygon_delete ( @@ -277,7 +262,6 @@ CREATE TABLE import_polygon_delete ( type TEXT NOT NULL ); CREATE INDEX idx_import_polygon_delete_osmid ON import_polygon_delete USING BTREE (osm_type, osm_id); -GRANT SELECT ON import_polygon_delete TO "{{config.DATABASE_WEBUSER}}"; DROP SEQUENCE IF EXISTS file; CREATE SEQUENCE file start 1; @@ -308,5 +292,3 @@ CREATE INDEX planet_osm_rels_relation_members_idx ON planet_osm_rels USING gin(p CREATE INDEX IF NOT EXISTS idx_place_interpolations ON place USING gist(geometry) {{db.tablespace.address_index}} WHERE osm_type = 'W' and address ? 'interpolation'; - -GRANT SELECT ON table country_osm_grid to "{{config.DATABASE_WEBUSER}}"; diff --git a/lib-sql/tiger_import_finish.sql b/lib-sql/tiger_import_finish.sql index b7c32d72..914677bd 100644 --- a/lib-sql/tiger_import_finish.sql +++ b/lib-sql/tiger_import_finish.sql @@ -13,8 +13,6 @@ CREATE INDEX IF NOT EXISTS idx_location_property_tiger_parent_place_id_imp CREATE UNIQUE INDEX IF NOT EXISTS idx_location_property_tiger_place_id_imp ON location_property_tiger_import (place_id) {{db.tablespace.aux_index}}; -GRANT SELECT ON location_property_tiger_import TO "{{config.DATABASE_WEBUSER}}"; - DROP TABLE IF EXISTS location_property_tiger; ALTER TABLE location_property_tiger_import RENAME TO location_property_tiger; diff --git a/src/nominatim_db/tokenizer/icu_tokenizer.py b/src/nominatim_db/tokenizer/icu_tokenizer.py index 5d90bb27..2ddfd8e3 100644 --- a/src/nominatim_db/tokenizer/icu_tokenizer.py +++ b/src/nominatim_db/tokenizer/icu_tokenizer.py @@ -144,10 +144,6 @@ class ICUTokenizer(AbstractTokenizer): with conn.cursor() as cur: cur.execute('SET max_parallel_workers_per_gather TO 0') - sqlp = SQLPreprocessor(conn, config) - sqlp.run_string(conn, - 'GRANT SELECT ON tmp_word TO "{{config.DATABASE_WEBUSER}}"') - conn.commit() self._create_base_indices(config, 'tmp_word') self._create_lookup_indices(config, 'tmp_word') self._move_temporary_word_table('tmp_word') @@ -245,11 +241,9 @@ class ICUTokenizer(AbstractTokenizer): word text, info jsonb ) {{db.tablespace.search_data}}; - GRANT SELECT ON word TO "{{config.DATABASE_WEBUSER}}"; DROP SEQUENCE IF EXISTS seq_word; CREATE SEQUENCE seq_word start 1; - GRANT SELECT ON seq_word to "{{config.DATABASE_WEBUSER}}"; """) conn.commit() diff --git a/src/nominatim_db/tools/migration.py b/src/nominatim_db/tools/migration.py index e1edc975..a2bee6b6 100644 --- a/src/nominatim_db/tools/migration.py +++ b/src/nominatim_db/tools/migration.py @@ -137,7 +137,6 @@ def create_placex_entrance_table(conn: Connection, config: Configuration, **_: A ); CREATE UNIQUE INDEX idx_placex_entrance_place_id_osm_id ON placex_entrance USING BTREE (place_id, osm_id) {{db.tablespace.search_index}}; - GRANT SELECT ON placex_entrance TO "{{config.DATABASE_WEBUSER}}" ; """) @@ -250,8 +249,6 @@ def create_place_postcode_table(conn: Connection, config: Configuration, **_: An geometry Geometry(Geometry, 4326) NOT NULL ) """) - sqlp.run_string(conn, - 'GRANT SELECT ON location_postcodes TO "{{config.DATABASE_WEBUSER}}"') # remove postcodes from the various auxillary tables cur.execute( """ From bf0ee6685b754b193f58389d978911cfe8c1fbc5 Mon Sep 17 00:00:00 2001 From: Itz-Agasta Date: Tue, 27 Jan 2026 17:53:25 +0530 Subject: [PATCH 2/6] Grants read-only access after import Adds execution of grant statements to provide read-only privileges for the web user following table creation or via a dedicated function. Facilitates easier post-import permission management. --- src/nominatim_db/tools/admin.py | 10 ++++++++++ src/nominatim_db/tools/database_import.py | 2 ++ 2 files changed, 12 insertions(+) diff --git a/src/nominatim_db/tools/admin.py b/src/nominatim_db/tools/admin.py index b8e3cb56..15446bb7 100644 --- a/src/nominatim_db/tools/admin.py +++ b/src/nominatim_db/tools/admin.py @@ -16,6 +16,7 @@ from psycopg.types.json import Json from ..typing import DictCursorResult from ..config import Configuration from ..db.connection import connect, Cursor, register_hstore +from ..db.sql_preprocessor import SQLPreprocessor from ..errors import UsageError from ..tokenizer import factory as tokenizer_factory from ..data.place_info import PlaceInfo @@ -105,3 +106,12 @@ def clean_deleted_relations(config: Configuration, age: str) -> None: except psycopg.DataError as exc: raise UsageError('Invalid PostgreSQL time interval format') from exc conn.commit() + + +def grant_ro_access(dsn: str, config: Configuration) -> None: + """ Grant read-only access to the web user for all Nominatim tables. + This can be used to grant access to a different user after import. + """ + with connect(dsn) as conn: + sql = SQLPreprocessor(conn, config) + sql.run_sql_file(conn, 'grants.sql') diff --git a/src/nominatim_db/tools/database_import.py b/src/nominatim_db/tools/database_import.py index c92c3900..f079e1fe 100644 --- a/src/nominatim_db/tools/database_import.py +++ b/src/nominatim_db/tools/database_import.py @@ -157,6 +157,8 @@ def create_tables(conn: Connection, config: Configuration, reverse_only: bool = sql.run_sql_file(conn, 'tables.sql') + sql.run_sql_file(conn, 'grants.sql') + def create_table_triggers(conn: Connection, config: Configuration) -> None: """ Create the triggers for the tables. The trigger functions must already From 58cae7059688ca7168ff9e4bf77de632997e8746 Mon Sep 17 00:00:00 2001 From: Itz-Agasta Date: Tue, 27 Jan 2026 17:54:10 +0530 Subject: [PATCH 3/6] Adds option to grant web user read-only DB access Introduces a command-line flag to grant read-only access to the web user for all tables, improving ease of permissions management during refresh operations. --- src/nominatim_db/clicmd/args.py | 1 + src/nominatim_db/clicmd/refresh.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/src/nominatim_db/clicmd/args.py b/src/nominatim_db/clicmd/args.py index ee9d8fec..a7072d9f 100644 --- a/src/nominatim_db/clicmd/args.py +++ b/src/nominatim_db/clicmd/args.py @@ -119,6 +119,7 @@ class NominatimArgs: enable_debug_statements: bool data_object: Sequence[Tuple[str, int]] data_area: Sequence[Tuple[str, int]] + ro_access: bool # Arguments to 'replication' init: bool diff --git a/src/nominatim_db/clicmd/refresh.py b/src/nominatim_db/clicmd/refresh.py index 1d1977d2..96646c1a 100644 --- a/src/nominatim_db/clicmd/refresh.py +++ b/src/nominatim_db/clicmd/refresh.py @@ -65,6 +65,8 @@ class UpdateRefresh: help='Update secondary importance raster data') group.add_argument('--importance', action='store_true', help='Recompute place importances (expensive!)') + group.add_argument('--ro-access', action='store_true', + help='Grant read-only access to web user for all tables') group.add_argument('--website', action='store_true', help='DEPRECATED. This function has no function anymore' ' and will be removed in a future version.') @@ -159,6 +161,11 @@ class UpdateRefresh: LOG.error('WARNING: Website setup is no longer required. ' 'This function will be removed in future version of Nominatim.') + if args.ro_access: + from ..tools import admin + LOG.warning('Grant read-only access to web user') + admin.grant_ro_access(args.config.get_libpq_dsn(), args.config) + if args.data_object or args.data_area: with connect(args.config.get_libpq_dsn()) as conn: for obj in args.data_object or []: From 5e2ce10fe0d8b227d626aea57797e6f8f1302dfc Mon Sep 17 00:00:00 2001 From: Itz-Agasta Date: Tue, 27 Jan 2026 17:55:51 +0530 Subject: [PATCH 4/6] Adds mock grants SQL file for import test --- test/python/tools/test_database_import.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/python/tools/test_database_import.py b/test/python/tools/test_database_import.py index f3d388da..413fac56 100644 --- a/test/python/tools/test_database_import.py +++ b/test/python/tools/test_database_import.py @@ -201,6 +201,8 @@ class TestSetupSQL: """CREATE FUNCTION test() RETURNS bool AS $$ SELECT {{db.reverse_only}} $$ LANGUAGE SQL""") + self.write_sql('grants.sql', "-- Mock grants file for testing\n") + database_import.create_tables(temp_db_conn, self.config, reverse) temp_db_cursor.scalar('SELECT test()') == reverse From e021f558bf1a698bcfed1aca061f2100da0621b5 Mon Sep 17 00:00:00 2001 From: Itz-Agasta Date: Fri, 30 Jan 2026 20:43:57 +0530 Subject: [PATCH 5/6] Restore grants for dynamic tables in tokenizer, migration, and tiger import --- lib-sql/grants.sql | 5 ----- lib-sql/tiger_import_finish.sql | 2 ++ src/nominatim_db/tokenizer/icu_tokenizer.py | 6 ++++++ src/nominatim_db/tools/migration.py | 3 +++ 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib-sql/grants.sql b/lib-sql/grants.sql index 6e26eaa8..e7ce878d 100644 --- a/lib-sql/grants.sql +++ b/lib-sql/grants.sql @@ -42,9 +42,4 @@ GRANT SELECT ON country_osm_grid TO "{{config.DATABASE_WEBUSER}}"; -- Tokenizer tables (word table) {% if 'word' in db.tables %} GRANT SELECT ON word TO "{{config.DATABASE_WEBUSER}}"; -{% endif %} - --- Tiger import table (if exists) -{% if 'location_property_tiger_import' in db.tables %} -GRANT SELECT ON location_property_tiger_import TO "{{config.DATABASE_WEBUSER}}"; {% endif %} \ No newline at end of file diff --git a/lib-sql/tiger_import_finish.sql b/lib-sql/tiger_import_finish.sql index 914677bd..b7c32d72 100644 --- a/lib-sql/tiger_import_finish.sql +++ b/lib-sql/tiger_import_finish.sql @@ -13,6 +13,8 @@ CREATE INDEX IF NOT EXISTS idx_location_property_tiger_parent_place_id_imp CREATE UNIQUE INDEX IF NOT EXISTS idx_location_property_tiger_place_id_imp ON location_property_tiger_import (place_id) {{db.tablespace.aux_index}}; +GRANT SELECT ON location_property_tiger_import TO "{{config.DATABASE_WEBUSER}}"; + DROP TABLE IF EXISTS location_property_tiger; ALTER TABLE location_property_tiger_import RENAME TO location_property_tiger; diff --git a/src/nominatim_db/tokenizer/icu_tokenizer.py b/src/nominatim_db/tokenizer/icu_tokenizer.py index 2ddfd8e3..5d90bb27 100644 --- a/src/nominatim_db/tokenizer/icu_tokenizer.py +++ b/src/nominatim_db/tokenizer/icu_tokenizer.py @@ -144,6 +144,10 @@ class ICUTokenizer(AbstractTokenizer): with conn.cursor() as cur: cur.execute('SET max_parallel_workers_per_gather TO 0') + sqlp = SQLPreprocessor(conn, config) + sqlp.run_string(conn, + 'GRANT SELECT ON tmp_word TO "{{config.DATABASE_WEBUSER}}"') + conn.commit() self._create_base_indices(config, 'tmp_word') self._create_lookup_indices(config, 'tmp_word') self._move_temporary_word_table('tmp_word') @@ -241,9 +245,11 @@ class ICUTokenizer(AbstractTokenizer): word text, info jsonb ) {{db.tablespace.search_data}}; + GRANT SELECT ON word TO "{{config.DATABASE_WEBUSER}}"; DROP SEQUENCE IF EXISTS seq_word; CREATE SEQUENCE seq_word start 1; + GRANT SELECT ON seq_word to "{{config.DATABASE_WEBUSER}}"; """) conn.commit() diff --git a/src/nominatim_db/tools/migration.py b/src/nominatim_db/tools/migration.py index a2bee6b6..e1edc975 100644 --- a/src/nominatim_db/tools/migration.py +++ b/src/nominatim_db/tools/migration.py @@ -137,6 +137,7 @@ def create_placex_entrance_table(conn: Connection, config: Configuration, **_: A ); CREATE UNIQUE INDEX idx_placex_entrance_place_id_osm_id ON placex_entrance USING BTREE (place_id, osm_id) {{db.tablespace.search_index}}; + GRANT SELECT ON placex_entrance TO "{{config.DATABASE_WEBUSER}}" ; """) @@ -249,6 +250,8 @@ def create_place_postcode_table(conn: Connection, config: Configuration, **_: An geometry Geometry(Geometry, 4326) NOT NULL ) """) + sqlp.run_string(conn, + 'GRANT SELECT ON location_postcodes TO "{{config.DATABASE_WEBUSER}}"') # remove postcodes from the various auxillary tables cur.execute( """ From 45972811e33b71efdcc3c32422166a1592ea8b6a Mon Sep 17 00:00:00 2001 From: Itz-Agasta Date: Sat, 31 Jan 2026 22:50:18 +0530 Subject: [PATCH 6/6] Preserve import error tables during freeze - Remove 'import_polygon_%' from UPDATE_TABLES to keep import_polygon_error and import_polygon_delete tables in frozen databases. - These tables contain permanent import error tracking data and should not be deleted during freeze. The ro-access grant system expects them to exist in all database states. --- lib-sql/grants.sql | 14 ++++++++------ src/nominatim_db/tools/freeze.py | 1 - 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib-sql/grants.sql b/lib-sql/grants.sql index e7ce878d..58e41061 100644 --- a/lib-sql/grants.sql +++ b/lib-sql/grants.sql @@ -15,7 +15,6 @@ GRANT SELECT ON nominatim_properties TO "{{config.DATABASE_WEBUSER}}"; -- Location tables GRANT SELECT ON location_property_tiger TO "{{config.DATABASE_WEBUSER}}"; GRANT SELECT ON location_property_osmline TO "{{config.DATABASE_WEBUSER}}"; -GRANT SELECT ON location_area TO "{{config.DATABASE_WEBUSER}}"; GRANT SELECT ON location_postcodes TO "{{config.DATABASE_WEBUSER}}"; -- Search tables @@ -28,10 +27,6 @@ GRANT SELECT ON placex TO "{{config.DATABASE_WEBUSER}}"; GRANT SELECT ON place_addressline TO "{{config.DATABASE_WEBUSER}}"; GRANT SELECT ON placex_entrance TO "{{config.DATABASE_WEBUSER}}"; --- OSM data tables -GRANT SELECT ON planet_osm_ways TO "{{config.DATABASE_WEBUSER}}"; -GRANT SELECT ON planet_osm_rels TO "{{config.DATABASE_WEBUSER}}"; - -- Error/delete tracking tables GRANT SELECT ON import_polygon_error TO "{{config.DATABASE_WEBUSER}}"; GRANT SELECT ON import_polygon_delete TO "{{config.DATABASE_WEBUSER}}"; @@ -42,4 +37,11 @@ GRANT SELECT ON country_osm_grid TO "{{config.DATABASE_WEBUSER}}"; -- Tokenizer tables (word table) {% if 'word' in db.tables %} GRANT SELECT ON word TO "{{config.DATABASE_WEBUSER}}"; -{% endif %} \ No newline at end of file +{% endif %} + +-- Special phrase tables +{% for table in db.tables %} +{% if table.startswith('place_classtype_') %} +GRANT SELECT ON {{ table }} TO "{{config.DATABASE_WEBUSER}}"; +{% endif %} +{% endfor %} \ No newline at end of file diff --git a/src/nominatim_db/tools/freeze.py b/src/nominatim_db/tools/freeze.py index 92bcc748..32d707e1 100644 --- a/src/nominatim_db/tools/freeze.py +++ b/src/nominatim_db/tools/freeze.py @@ -18,7 +18,6 @@ UPDATE_TABLES = [ 'address_levels', 'gb_postcode', 'import_osmosis_log', - 'import_polygon_%', 'location_area%', 'location_road%', 'place',