diff --git a/lib-sql/grants.sql b/lib-sql/grants.sql new file mode 100644 index 00000000..58e41061 --- /dev/null +++ b/lib-sql/grants.sql @@ -0,0 +1,47 @@ +-- 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_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}}"; + +-- 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 %} + +-- 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/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/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 []: 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 0b26878f..2131a88b 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 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', 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