changeset 195:5dc8e734487a

Introduce database schemas as privilege-based namespaces Some privileges changed (e.g. for responsibility_areas), but additional privileges were not left off intentionally before. Search path settings have been replaced by schema-qualifying names in statements to prevent object definitions from being dependend on search path settings.
author Tom Gottfried <tom@intevation.de>
date Fri, 20 Jul 2018 17:28:16 +0200
parents cf5dcc1761df
children b67208d82543
files schema/auth.sql schema/auth_tests.sql schema/gemma.sql schema/manage_users.sql schema/manage_users_tests.sql schema/tap_tests_data.sql
diffstat 6 files changed, 387 insertions(+), 371 deletions(-) [+]
line wrap: on
line diff
--- a/schema/auth.sql	Fri Jul 20 18:32:40 2018 +0200
+++ b/schema/auth.sql	Fri Jul 20 17:28:16 2018 +0200
@@ -3,7 +3,6 @@
 --
 -- Roles, privileges and policies for the GEMMA database
 --
-SET search_path TO public, gemma, gemma_waterway, gemma_fairway;
 
 -- We do not want any users to be able to create any objects
 REVOKE CREATE ON SCHEMA public FROM PUBLIC;
@@ -11,25 +10,24 @@
 --
 -- Privileges for waterway_user
 --
-GRANT USAGE ON SCHEMA gemma, gemma_waterway, gemma_fairway TO waterway_user;
-GRANT SELECT ON ALL TABLES IN SCHEMA gemma_waterway, gemma_fairway
-    TO waterway_user;
-GRANT SELECT ON templates, user_templates, user_profiles TO waterway_user;
+GRANT USAGE ON SCHEMA users, waterway TO waterway_user;
+GRANT SELECT ON ALL TABLES IN SCHEMA public, users, waterway TO waterway_user;
 
 --
 -- Extended privileges for waterway_admin
 --
-GRANT INSERT, UPDATE ON ALL TABLES IN SCHEMA gemma_waterway, gemma_fairway
-    TO waterway_admin;
+GRANT INSERT, UPDATE ON ALL TABLES IN SCHEMA waterway TO waterway_admin;
 -- TODO: will there ever be UPDATEs or can we drop that due to historicisation?
-GRANT INSERT, UPDATE, DELETE ON templates, user_templates TO waterway_admin;
-GRANT SELECT ON responsibility_areas TO waterway_admin;
+GRANT INSERT, UPDATE, DELETE ON
+    users.templates, users.user_templates TO waterway_admin;
 
 --
 -- Extended privileges for sys_admin
 --
-GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA gemma
-    TO sys_admin;
+GRANT INSERT, UPDATE, DELETE
+    ON users.user_profiles, users.responsibility_areas TO sys_admin;
+GRANT USAGE ON SCHEMA sys_admin TO sys_admin;
+GRANT UPDATE ON sys_admin.system_config TO sys_admin;
 
 --
 -- RLS policies for waterway_user
@@ -52,9 +50,10 @@
         'bottlenecks', 'sounding_results']
     -- TODO: add all relevant tables here
     LOOP
-        EXECUTE format('CREATE POLICY hide_staging ON %I '
+        EXECUTE format('CREATE POLICY hide_staging ON waterway.%I '
             'FOR SELECT TO waterway_user USING (staging_done)', the_table);
-        EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', the_table);
+        EXECUTE format('ALTER TABLE waterway.%I ENABLE ROW LEVEL SECURITY',
+            the_table);
     END LOOP;
 END;
 $$
@@ -62,18 +61,18 @@
 SELECT create_hide_staging_policy();
 DROP FUNCTION create_hide_staging_policy;
 
-CREATE POLICY see_yourself ON user_profiles FOR SELECT TO waterway_user
+CREATE POLICY see_yourself ON users.user_profiles FOR SELECT TO waterway_user
     USING (username = current_user);
-ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY;
+ALTER TABLE users.user_profiles ENABLE ROW LEVEL SECURITY;
 
-CREATE POLICY user_templates ON user_templates FOR ALL TO waterway_user
-    USING (username IN(SELECT username FROM user_profiles));
-ALTER TABLE user_templates ENABLE ROW LEVEL SECURITY;
+CREATE POLICY user_templates ON users.user_templates FOR ALL TO waterway_user
+    USING (username IN(SELECT username FROM users.user_profiles));
+ALTER TABLE users.user_templates ENABLE ROW LEVEL SECURITY;
 
-CREATE POLICY user_templates ON templates FOR ALL TO waterway_user
-    USING (template_name IN(SELECT template_name FROM user_templates))
+CREATE POLICY user_templates ON users.templates FOR ALL TO waterway_user
+    USING (template_name IN(SELECT template_name FROM users.user_templates))
     WITH CHECK (true);
-ALTER TABLE templates ENABLE ROW LEVEL SECURITY;
+ALTER TABLE users.templates ENABLE ROW LEVEL SECURITY;
 
 --
 -- RLS policies for waterway_admin
@@ -82,22 +81,27 @@
 -- Security-definer function to get current users country, which allows to
 -- restrict the view on user_profiles by country without infinite recursion
 CREATE FUNCTION current_user_country()
-    RETURNS gemma.user_profiles.country%TYPE
-    AS $$ SELECT country FROM user_profiles WHERE username = session_user $$
+    RETURNS users.user_profiles.country%TYPE
+    AS $$
+        SELECT country FROM users.user_profiles WHERE username = session_user
+    $$
     LANGUAGE SQL
     SECURITY DEFINER
     STABLE PARALLEL SAFE;
 
 -- Staging area
 -- TODO: add all relevant tables here
-CREATE POLICY responsibility_area ON bottlenecks FOR ALL TO waterway_admin
-    USING (ST_Within(area, (SELECT area FROM responsibility_areas
+CREATE POLICY responsibility_area ON waterway.bottlenecks
+    FOR ALL TO waterway_admin
+    USING (ST_Within(area, (SELECT area FROM users.responsibility_areas
         WHERE country = current_user_country())));
-CREATE POLICY responsibility_area ON sounding_results FOR ALL TO waterway_admin
-    USING (ST_Within(area, (SELECT area FROM responsibility_areas
+CREATE POLICY responsibility_area ON waterway.sounding_results
+    FOR ALL TO waterway_admin
+    USING (ST_Within(area, (SELECT area FROM users.responsibility_areas
         WHERE country = current_user_country())));
 
-CREATE POLICY country_profiles ON user_profiles FOR SELECT TO waterway_admin
+CREATE POLICY country_profiles ON users.user_profiles
+    FOR SELECT TO waterway_admin
     USING (country = current_user_country());
 
 COMMIT;
--- a/schema/auth_tests.sql	Fri Jul 20 18:32:40 2018 +0200
+++ b/schema/auth_tests.sql	Fri Jul 20 17:28:16 2018 +0200
@@ -3,8 +3,6 @@
 --
 SELECT plan(16); -- Give number of tests that have to be run
 
-SET search_path TO public, gemma, gemma_waterway, gemma_fairway;
-
 --
 -- Run tests as unprivileged user
 --
@@ -13,21 +11,21 @@
 SELECT throws_ok('CREATE TABLE test()', 42501, NULL,
                  'No objects can be created');
 
-SELECT isnt_empty('SELECT * FROM bottlenecks',
+SELECT isnt_empty('SELECT * FROM waterway.bottlenecks',
                   'Staged data should be visible');
-SELECT is_empty('SELECT * FROM bottlenecks WHERE NOT staging_done',
+SELECT is_empty('SELECT * FROM waterway.bottlenecks WHERE NOT staging_done',
                 'Only staged data should be visible');
 
-SELECT set_eq('SELECT count(*) FROM gemma.user_profiles', ARRAY[1],
+SELECT set_eq('SELECT count(*) FROM users.user_profiles', ARRAY[1],
               'User should only see his own profile');
-SELECT results_eq('SELECT username FROM user_profiles',
+SELECT results_eq('SELECT username FROM users.user_profiles',
                   'SELECT CAST(current_user AS varchar)',
                   'User should only see his own profile');
 
-SELECT isnt_empty('SELECT * FROM templates',
+SELECT isnt_empty('SELECT * FROM users.templates',
                   'User should see templates associated to him');
-SELECT is_empty('SELECT * FROM templates
-                 JOIN user_templates USING (template_name)
+SELECT is_empty('SELECT * FROM users.templates
+                 JOIN users.user_templates USING (template_name)
                  WHERE username <> current_user',
                 'User should only see templates associated to him');
 
@@ -37,7 +35,7 @@
 SET SESSION AUTHORIZATION waterway_admin;
 
 PREPARE bn_insert (varchar, geometry(POLYGON, 4326)) AS
-    INSERT INTO bottlenecks (
+    INSERT INTO waterway.bottlenecks (
         bottleneck_id, fk_g_fid, stretch, area, rb, lb, responsible_country,
         revisiting_time, limiting, source_organization)
         VALUES (
@@ -61,35 +59,35 @@
                  'Waterway admin cannot insert data outside his region');
 
 -- template management
-SELECT isnt_empty('SELECT * FROM templates
-                   JOIN user_templates USING (template_name)
+SELECT isnt_empty('SELECT * FROM users.templates
+                   JOIN users.user_templates USING (template_name)
                    WHERE username <> current_user',
                   'Waterway admin should see templates of other users');
 
-SELECT lives_ok('INSERT INTO templates (template_name, template_data)
+SELECT lives_ok('INSERT INTO users.templates (template_name, template_data)
                  VALUES (''New AT'', ''\x'');
-                 INSERT INTO user_templates
+                 INSERT INTO users.user_templates
                  VALUES (''waterway_user'', ''New AT'')',
                 'Waterway admin can add templates for users in his country');
 
-SELECT throws_ok('INSERT INTO user_templates
+SELECT throws_ok('INSERT INTO users.user_templates
                   VALUES (''waterway_user2'', ''AT'')',
                  42501, NULL,
                  'Waterway admin cannot add template for other country');
 
-SELECT isnt_empty('UPDATE templates SET template_data = ''\xDABE''
+SELECT isnt_empty('UPDATE users.templates SET template_data = ''\xDABE''
                    WHERE template_name = ''AT'' RETURNING *',
                   'Waterway admin can alter templates for own country');
 
-SELECT is_empty('UPDATE templates SET template_data = ''\xDABE''
+SELECT is_empty('UPDATE users.templates SET template_data = ''\xDABE''
                  WHERE template_name = ''RO'' RETURNING *',
                 'Waterway admin cannot alter templates for other country');
 
-SELECT isnt_empty('DELETE FROM templates WHERE template_name = ''AT''
+SELECT isnt_empty('DELETE FROM users.templates WHERE template_name = ''AT''
                    RETURNING *',
                   'Waterway admin can delete templates for own country');
 
-SELECT is_empty('DELETE FROM templates WHERE template_name = ''RO''
+SELECT is_empty('DELETE FROM users.templates WHERE template_name = ''RO''
                  RETURNING *',
                 'Waterway admin cannot delete templates for other country');
 
--- a/schema/gemma.sql	Fri Jul 20 18:32:40 2018 +0200
+++ b/schema/gemma.sql	Fri Jul 20 17:28:16 2018 +0200
@@ -35,96 +35,40 @@
 --
 -- GEMMA data
 --
-CREATE SCHEMA gemma;
-CREATE SCHEMA gemma_waterway;
-CREATE SCHEMA gemma_fairway;
-SET search_path TO public, gemma, gemma_waterway, gemma_fairway;
--- TODO: consolidate schemas. The current distribution of tables is mainly
--- for diagram generation. A privilege based distribution might ease
--- privilege management.
+
+-- Namespace to be accessed by sys_admin only
+CREATE SCHEMA sys_admin
+    CREATE TABLE sys_admin.system_config (
+        config_key varchar PRIMARY KEY,
+        config_val varchar
+    )
+;
 
 --
--- Auxiliary tables
+-- Look-up tables with data that are static in a running system
 --
-CREATE TABLE gemma.system_config (
-    config_key varchar PRIMARY KEY,
-    config_val varchar
-);
-
-CREATE TABLE gemma.countries (
+CREATE TABLE countries (
     country_code char(2) PRIMARY KEY -- ISO 3166 country code
     -- A smallint PK would require even less disk space i.e. on the FK side.
     -- This might be an issue in case cache space becomes a limiting
     -- factor when there are many FKs pointing here.
 );
 
-CREATE TABLE gemma.responsibility_areas (
-    country char(2) PRIMARY KEY REFERENCES countries,
-    area geometry(MULTIPOLYGON, 4326)
-    --XXX: Should be geography (elsewhere too)
-);
-
-CREATE TABLE gemma.language_codes (
+CREATE TABLE language_codes (
     language_code varchar PRIMARY KEY
 );
 
-CREATE TABLE gemma.user_profiles (
-    username varchar PRIMARY KEY, -- TODO: check it's in pg_roles by trigger
-    country char(2) NOT NULL REFERENCES countries,
-    map_extent box2d,
-    email_adress varchar NOT NULL UNIQUE
-);
-
-CREATE TABLE gemma.templates (
-    template_name varchar PRIMARY KEY,
-    template_data bytea NOT NULL,
-    date_info timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-CREATE TRIGGER templates_date_info BEFORE UPDATE ON templates
-    FOR EACH ROW EXECUTE PROCEDURE update_date_info();
-
-CREATE TABLE gemma.user_templates (
-    username varchar NOT NULL REFERENCES user_profiles ON DELETE CASCADE,
-    template_name varchar NOT NULL REFERENCES templates ON DELETE CASCADE,
-    PRIMARY KEY (username, template_name)
-);
-
---
--- General river information
---
-
--- Eventually obsolete.
--- See https://roundup-intern.intevation.de/gemma/issue5
--- CREATE TABLE rwdrs (
---        stretch isrsrange PRIMARY KEY,
---        -- https://www.postgresql.org/docs/10/static/sql-createindex.html:
---        -- Only B-tree supports UNIQUE indexes, but we need the GIST index
---        -- below anyhow.
---        -- Is it a good idea to build B-tree indexes on relatively large
---        -- composites of string values or should we use inter PKs?
---        -- => In case the index is used and cache space becomes a limiting
---        -- factor, this might be an issue.
---        rwdr double precision NOT NULL,
---        EXCLUDE USING GIST (stretch WITH &&)
---);
-
-CREATE TABLE gemma_waterway.catccls (
+CREATE TABLE catccls (
     catccl smallint PRIMARY KEY
     -- TODO: Do we need name and/or definition from IENC feature catalogue?
 );
 
-CREATE TABLE gemma_waterway.dirimps (
+CREATE TABLE dirimps (
     dirimp smallint PRIMARY KEY
     -- TODO: Do we need name and/or definition from IENC feature catalogue?
 );
 
-CREATE TABLE gemma_waterway.waterway_area (
-    area geometry(POLYGON, 4326) PRIMARY KEY,
-    catccl smallint REFERENCES catccls,
-    dirimp smallint REFERENCES dirimps
-);
-
-CREATE TABLE gemma_fairway.depth_references (
+CREATE TABLE depth_references (
     depth_reference varchar(4) PRIMARY KEY
     -- See col. AB and AI RIS-Index Encoding Guide
     -- XXX: We need a way to distinguish between geodetic (eg. col. AP
@@ -135,76 +79,16 @@
     --   (which would mean a model differing a bit from RIS-Index ideas)
 );
 
-CREATE TABLE gemma_fairway.reference_water_levels (
+CREATE TABLE reference_water_levels (
     reference_water_level varchar(20) PRIMARY KEY
 );
 
-CREATE TABLE gemma_fairway.gauges (
-    location isrs PRIMARY KEY,
-    function_code varchar(10) NOT NULL, -- XXX: What is this really for?
-    objname varchar NOT NULL,
-    is_left boolean, -- XXX: Or reference position_codes?
-    geom geometry(POINT, 4326) NOT NULL,
-    applicability isrsrange,
-    validity tstzrange, -- XXX: Should ranges be NOT NULL? In DRC, only copy
-    -- pasted text from a more general specification is given
-    -- (a gauge is not a berth!)
-    -- TODO: Ranges need a joint exclusion constaint to prevent overlaps?
-    zero_point double precision NOT NULL,
-    geodref varchar(4) REFERENCES depth_references,
-    date_info timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    source_organization varchar NOT NULL
-);
-CREATE TRIGGER gauges_date_info BEFORE UPDATE ON gauges
-    FOR EACH ROW EXECUTE PROCEDURE update_date_info();
-
-CREATE TABLE gemma_fairway.gauges_reference_water_levels (
-    gauge_id isrs NOT NULL REFERENCES gauges,
-    reference_water_level varchar(20)
-        NOT NULL REFERENCES reference_water_levels,
-    PRIMARY KEY (gauge_id, reference_water_level),
-    value int NOT NULL
-);
-
-CREATE TABLE gemma_fairway.gauge_measurements (
-    fk_gauge_id isrs NOT NULL REFERENCES gauges,
-    measure_date timestamp with time zone NOT NULL,
-    PRIMARY KEY (fk_gauge_id, measure_date),
-    -- XXX: Is country_code really relevant for GEMMA or just NtS?
-    -- country_code char(2) NOT NULL REFERENCES countries,
-    -- TODO: add relations to stuff provided as enumerations
-    sender varchar NOT NULL, -- "from" attribute from DRC
-    language_code varchar NOT NULL REFERENCES language_codes,
-    date_issue timestamp with time zone NOT NULL,
-    -- reference_code varchar(4) NOT NULL REFERENCES depth_references,
-    -- XXX: Always ZPG?
-    water_level double precision NOT NULL,
-    predicted boolean NOT NULL,
-    is_waterlevel boolean NOT NULL,
-    -- XXX: "measure_code" if really only W or Q
-    -- XXX: Do we need "unit" attribute or can we normalise on import?
-    value_min double precision, -- XXX: NOT NULL if predicted?
-    value_max double precision, -- XXX: NOT NULL if predicted?
-    --- TODO: Add a double range type for checking?
-    date_info timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    source_organization varchar NOT NULL -- "originator"
-);
-CREATE TRIGGER gauge_measurements_date_info BEFORE UPDATE ON gauge_measurements
-    FOR EACH ROW EXECUTE PROCEDURE update_date_info();
-
-CREATE TABLE gemma_waterway.waterway_axis (
-    wtwaxs geometry(LINESTRING, 4326) PRIMARY KEY,
-    -- TODO: Do we need to check data set quality as described in DRC 2.1.6?
-    objnam varchar NOT NULL,
-    nobjnam varchar
-);
-
-CREATE TABLE gemma_waterway.distance_mark_functions (
+CREATE TABLE distance_mark_functions (
     -- XXX: Redundant information to object code in isrs code of dist. mark
     distance_mark_function varchar(8) PRIMARY KEY
 );
 
-CREATE TABLE gemma_waterway.position_codes (
+CREATE TABLE position_codes (
     position_code char(2) PRIMARY KEY
     -- Use smallint because of fairway availability provided on daily basis?
     -- Data come from 2015_06_23_RIS_Index_template_v2p0.xlsx,
@@ -214,197 +98,319 @@
     -- TODO: Do we need an attribute "meaning" or so?
 );
 
--- This table allows linkage between the 1D ISRS location codes and 2D space
--- e.g. for cutting bottleneck area out of waterway area based on virtual
--- distance marks along waterway axis (see SUC7).
-CREATE TABLE gemma_waterway.distance_marks (
-    location_code isrs PRIMARY KEY,
-    geom geometry(POINT, 4326) NOT NULL,
-    distance_mark_function varchar(8)
-        NOT NULL REFERENCES distance_mark_functions,
-    -- XXX: attribute "function" in DRC 2.1.7 as well as CATDIS seem
-    -- to encode the same thing as the object code in ISRS location code.
-    position_code char(2) NOT NULL REFERENCES position_codes
-);
-
-CREATE TABLE gemma_waterway.sections_stretches (
-    id varchar PRIMARY KEY,
-    is_section boolean NOT NULL, -- maps 'function' from interface
-    stretch isrsrange,
-    objnam varchar NOT NULL,
-    nobjnam varchar,
-    date_info timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    source_organization varchar NOT NULL
-);
-CREATE TRIGGER sections_stretches_date_info BEFORE UPDATE ON sections_stretches
-    FOR EACH ROW EXECUTE PROCEDURE update_date_info();
-
-CREATE TABLE gemma_waterway.waterway_profiles (
-    location isrs NOT NULL,
-    validity tstzrange,
-    EXCLUDE USING GIST (validity WITH &&),
-    PRIMARY KEY (location, validity),
-    lnwl smallint,
-    mwl smallint,
-    hnwl smallint,
-    fe30 smallint,
-    fe100 smallint,
-    -- XXX: further normalise using reference_water_levels?
-    CHECK(COALESCE(lnwl, mwl, hnwl, fe30, fe100) IS NULL
-        OR validity IS NOT NULL),
-    date_info timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    source_organization varchar NOT NULL
-);
-CREATE TRIGGER waterway_profiles_date_info BEFORE UPDATE ON waterway_profiles
-    FOR EACH ROW EXECUTE PROCEDURE update_date_info();
-
-CREATE TABLE gemma_waterway.levels_of_service (
+CREATE TABLE levels_of_service (
     level_of_service smallint PRIMARY KEY
 );
 
-CREATE TABLE gemma_waterway.fairway_dimensions (
-    area geometry(POLYGON, 4326) PRIMARY KEY,
-    level_of_service smallint NOT NULL REFERENCES levels_of_service,
-    min_width smallint NOT NULL,
-    max_width smallint NOT NULL,
-    min_depth smallint NOT NULL,
-    date_info timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    source_organization varchar NOT NULL
-);
-CREATE TRIGGER fairway_dimensions_date_info BEFORE UPDATE ON fairway_dimensions
-    FOR EACH ROW EXECUTE PROCEDURE update_date_info();
-
---
--- Bottlenecks
---
-CREATE TABLE gemma_fairway.riverbed_materials (
+CREATE TABLE riverbed_materials (
     material varchar PRIMARY KEY
     -- XXX: Should this table contain choices from DRC 2.2.3 or
     -- from IENC Encoding Guide M.4.3, attribute NATSUR?
 );
 
-CREATE TABLE gemma_fairway.survey_types (
+CREATE TABLE survey_types (
     survey_type varchar PRIMARY KEY
 );
 
-CREATE TABLE gemma_fairway.coverage_types (
+CREATE TABLE coverage_types (
     coverage_type varchar PRIMARY KEY
 );
 
-CREATE TABLE gemma_fairway.limiting_factors (
+CREATE TABLE limiting_factors (
     limiting_factor varchar PRIMARY KEY
 );
 
--- XXX: Nullability differs between DRC (attributes marked "O") and WSDL
--- (minOccurs=0; nillable seems to be set arbitrarily as even bottleneck_id and
--- fk_g_fid (both mandatory, i.e. marked "M" in DRC) have nillable="true" in WSDL)
-CREATE TABLE gemma_fairway.bottlenecks (
-    bottleneck_id varchar PRIMARY KEY,
-    fk_g_fid isrs NOT NULL REFERENCES gauges,
-    -- XXX: DRC references "ch. 3.1.1", which does not exist in document.
-    objnam varchar,
-    nobjnm varchar,
-    stretch isrsrange NOT NULL,
-    area geometry(POLYGON, 4326) NOT NULL,
-    rb char(2) NOT NULL REFERENCES countries, -- from rb_lb in interface
-    lb char(2) NOT NULL REFERENCES countries, -- from rb_lb in interface
-    responsible_country char(2) NOT NULL REFERENCES countries,
-    revisiting_time smallint NOT NULL,
-    limiting varchar NOT NULL REFERENCES limiting_factors,
-    -- surtyp varchar NOT NULL REFERENCES survey_types,
-    -- XXX: Also an attribut of sounding result?
-    -- coverage varchar REFERENCES coverage_types,
-    -- XXX: Also an attribut of sounding result?
-    -- CHECK allowed combinations of surtyp and coverage or
-    -- different model approach?
-    -- depth_reference char(3) NOT NULL REFERENCES depth_references,
-    -- XXX: Also an attribut of sounding result?
-    date_info timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    source_organization varchar NOT NULL,
-    -- additional_data xml -- Currently not relevant for GEMMA
-    staging_done boolean NOT NULL DEFAULT false
-);
-CREATE TRIGGER bottleneck_date_info BEFORE UPDATE ON bottlenecks
-    FOR EACH ROW EXECUTE PROCEDURE update_date_info();
-
-CREATE TABLE gemma_fairway.bottlenecks_riverbed_materials (
-    bottleneck_id varchar REFERENCES bottlenecks,
-    riverbed varchar REFERENCES riverbed_materials,
-    -- XXX: should be 'natsur' according to IENC Encoding Guide M.4.3
-    PRIMARY KEY (bottleneck_id, riverbed)
-);
-
-CREATE TABLE gemma_fairway.sounding_results (
-    bottleneck_id varchar NOT NULL REFERENCES bottlenecks,
-    date_info date NOT NULL,
-    PRIMARY KEY (bottleneck_id, date_info),
-    area geometry(POLYGON, 4326) NOT NULL,
-    surtyp varchar NOT NULL REFERENCES survey_types,
-    coverage varchar REFERENCES coverage_types,
-    depth_reference char(3) NOT NULL REFERENCES depth_references,
-    sounding_data raster NOT NULL,
-    staging_done boolean NOT NULL DEFAULT false
-);
-
---
--- Fairway availability
---
-CREATE TABLE gemma_fairway.measure_types (
+CREATE TABLE measure_types (
     measure_type varchar PRIMARY KEY
 );
 
-CREATE TABLE gemma_fairway.fairway_availability (
-    id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
-    position_code char(2) REFERENCES position_codes,
-    bottleneck_id varchar NOT NULL REFERENCES bottlenecks,
-    surdat date NOT NULL,
-    UNIQUE (bottleneck_id, surdat),
-    -- additional_data xml -- Currently not relevant for GEMMA
-    critical boolean,
-    date_info timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    source_organization varchar NOT NULL
-);
-CREATE TRIGGER fairway_availability_date_info
-    BEFORE UPDATE ON fairway_availability
-    FOR EACH ROW EXECUTE PROCEDURE update_date_info();
+
+-- Namespace for user management related data
+CREATE SCHEMA users
+    CREATE TABLE responsibility_areas (
+        country char(2) PRIMARY KEY REFERENCES countries,
+        area geometry(MULTIPOLYGON, 4326)
+        --XXX: Should be geography (elsewhere too)
+    )
+
+    CREATE TABLE user_profiles (
+        username varchar PRIMARY KEY,-- TODO: check it's in pg_roles by trigger
+        country char(2) NOT NULL REFERENCES countries,
+        map_extent box2d,
+        email_adress varchar NOT NULL UNIQUE
+    )
+
+    CREATE TABLE templates (
+        template_name varchar PRIMARY KEY,
+        template_data bytea NOT NULL,
+        date_info timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP
+    )
+    CREATE TRIGGER templates_date_info BEFORE UPDATE ON templates
+        FOR EACH ROW EXECUTE PROCEDURE update_date_info()
+
+    CREATE TABLE user_templates (
+        username varchar NOT NULL REFERENCES user_profiles ON DELETE CASCADE,
+        template_name varchar NOT NULL REFERENCES templates ON DELETE CASCADE,
+        PRIMARY KEY (username, template_name)
+    )
+;
+
+-- Namespace for waterway data that can change in a running system
+CREATE SCHEMA waterway
+
+    -- Eventually obsolete.
+    -- See https://roundup-intern.intevation.de/gemma/issue5
+    -- CREATE TABLE rwdrs (
+    --     tretch isrsrange PRIMARY KEY,
+    --     -- https://www.postgresql.org/docs/10/static/sql-createindex.html:
+    --     -- Only B-tree supports UNIQUE indexes, but we need the GIST index
+    --     -- below anyhow.
+    --     -- Is it a good idea to build B-tree indexes on relatively large
+    --     -- composites of string values or should we use inter PKs?
+    --     -- => In case the index is used and cache space becomes a limiting
+    --     -- factor, this might be an issue.
+    --     rwdr double precision NOT NULL,
+    --     EXCLUDE USING GIST (stretch WITH &&)
+    --)
+
+    CREATE TABLE waterway_area (
+        area geometry(POLYGON, 4326) PRIMARY KEY,
+        catccl smallint REFERENCES catccls,
+        dirimp smallint REFERENCES dirimps
+    )
 
-CREATE TABLE gemma_fairway.fa_reference_values (
-    fairway_availability_id int NOT NULL REFERENCES fairway_availability,
-    level_of_service smallint NOT NULL REFERENCES levels_of_service,
-    PRIMARY KEY (fairway_availability_id, level_of_service),
-    fairway_depth smallint,
-    fairway_width smallint,
-    fairway_radius int,
-    CHECK(COALESCE(fairway_depth, fairway_width, fairway_radius) IS NOT NULL),
-    shallowest_spot geometry(POINT, 4326)
-);
+    CREATE TABLE gauges (
+        location isrs PRIMARY KEY,
+        function_code varchar(10) NOT NULL, -- XXX: What is this really for?
+        objname varchar NOT NULL,
+        is_left boolean, -- XXX: Or reference position_codes?
+        geom geometry(POINT, 4326) NOT NULL,
+        applicability isrsrange,
+        validity tstzrange,-- XXX: Should ranges be NOT NULL? In DRC, only copy
+        -- pasted text from a more general specification is given
+        -- (a gauge is not a berth!)
+        -- TODO: Ranges need a joint exclusion constaint to prevent overlaps?
+        zero_point double precision NOT NULL,
+        geodref varchar(4) REFERENCES depth_references,
+        date_info timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
+        source_organization varchar NOT NULL
+    )
+    CREATE TRIGGER gauges_date_info BEFORE UPDATE ON gauges
+        FOR EACH ROW EXECUTE PROCEDURE update_date_info()
+
+    CREATE TABLE gauges_reference_water_levels (
+        gauge_id isrs NOT NULL REFERENCES gauges,
+        reference_water_level varchar(20)
+            NOT NULL REFERENCES reference_water_levels,
+        PRIMARY KEY (gauge_id, reference_water_level),
+        value int NOT NULL
+    )
+
+    CREATE TABLE gauge_measurements (
+        fk_gauge_id isrs NOT NULL REFERENCES gauges,
+        measure_date timestamp with time zone NOT NULL,
+        PRIMARY KEY (fk_gauge_id, measure_date),
+        -- XXX: Is country_code really relevant for GEMMA or just NtS?
+        -- country_code char(2) NOT NULL REFERENCES countries,
+        -- TODO: add relations to stuff provided as enumerations
+        sender varchar NOT NULL, -- "from" attribute from DRC
+        language_code varchar NOT NULL REFERENCES language_codes,
+        date_issue timestamp with time zone NOT NULL,
+        -- reference_code varchar(4) NOT NULL REFERENCES depth_references,
+        -- XXX: Always ZPG?
+        water_level double precision NOT NULL,
+        predicted boolean NOT NULL,
+        is_waterlevel boolean NOT NULL,
+        -- XXX: "measure_code" if really only W or Q
+        -- XXX: Do we need "unit" attribute or can we normalise on import?
+        value_min double precision, -- XXX: NOT NULL if predicted?
+        value_max double precision, -- XXX: NOT NULL if predicted?
+        --- TODO: Add a double range type for checking?
+        date_info timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
+        source_organization varchar NOT NULL -- "originator"
+    )
+    CREATE TRIGGER gauge_measurements_date_info
+        BEFORE UPDATE ON gauge_measurements
+        FOR EACH ROW EXECUTE PROCEDURE update_date_info()
+
+    CREATE TABLE waterway_axis (
+        wtwaxs geometry(LINESTRING, 4326) PRIMARY KEY,
+        -- TODO: Do we need to check data set quality (DRC 2.1.6)?
+        objnam varchar NOT NULL,
+        nobjnam varchar
+    )
+
+    -- This table allows linkage between 1D ISRS location codes and 2D space
+    -- e.g. for cutting bottleneck area out of waterway area based on virtual
+    -- distance marks along waterway axis (see SUC7).
+    CREATE TABLE distance_marks (
+        location_code isrs PRIMARY KEY,
+        geom geometry(POINT, 4326) NOT NULL,
+        distance_mark_function varchar(8)
+            NOT NULL REFERENCES distance_mark_functions,
+        -- XXX: attribute "function" in DRC 2.1.7 as well as CATDIS seem
+        -- to encode the same thing as the object code in ISRS location code.
+        position_code char(2) NOT NULL REFERENCES position_codes
+    )
+
+    CREATE TABLE sections_stretches (
+        id varchar PRIMARY KEY,
+        is_section boolean NOT NULL, -- maps 'function' from interface
+        stretch isrsrange,
+        objnam varchar NOT NULL,
+        nobjnam varchar,
+        date_info timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
+        source_organization varchar NOT NULL
+    )
+    CREATE TRIGGER sections_stretches_date_info
+        BEFORE UPDATE ON sections_stretches
+        FOR EACH ROW EXECUTE PROCEDURE update_date_info()
 
-CREATE TABLE gemma_fairway.bottleneck_pdfs (
-    fairway_availability_id int NOT NULL REFERENCES fairway_availability,
-    profile_pdf_filename varchar NOT NULL, -- redundant to last part of URL
-    profile_pdf_url varchar NOT NULL, -- prohibit insecure URLs somehow?
-    PRIMARY KEY (fairway_availability_id, profile_pdf_url),
-    pdf_generation_date timestamp with time zone NOT NULL,
-    source_organization varchar NOT NULL
-);
+    CREATE TABLE waterway_profiles (
+        location isrs NOT NULL,
+        validity tstzrange,
+        EXCLUDE USING GIST (validity WITH &&),
+        PRIMARY KEY (location, validity),
+        lnwl smallint,
+        mwl smallint,
+        hnwl smallint,
+        fe30 smallint,
+        fe100 smallint,
+        -- XXX: further normalise using reference_water_levels?
+        CHECK(COALESCE(lnwl, mwl, hnwl, fe30, fe100) IS NULL
+            OR validity IS NOT NULL),
+        date_info timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
+        source_organization varchar NOT NULL
+    )
+    CREATE TRIGGER waterway_profiles_date_info
+        BEFORE UPDATE ON waterway_profiles
+        FOR EACH ROW EXECUTE PROCEDURE update_date_info()
+
+    CREATE TABLE fairway_dimensions (
+        area geometry(POLYGON, 4326) PRIMARY KEY,
+        level_of_service smallint NOT NULL REFERENCES levels_of_service,
+        min_width smallint NOT NULL,
+        max_width smallint NOT NULL,
+        min_depth smallint NOT NULL,
+        date_info timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
+        source_organization varchar NOT NULL
+    )
+    CREATE TRIGGER fairway_dimensions_date_info
+        BEFORE UPDATE ON fairway_dimensions
+        FOR EACH ROW EXECUTE PROCEDURE update_date_info()
+
+    --
+    -- Bottlenecks
+    --
+    -- XXX: Nullability differs between DRC (attributes marked "O") and WSDL
+    -- (minOccurs=0; nillable seems to be set arbitrarily as even bottleneck_id and
+    -- fk_g_fid (both mandatory, i.e. marked "M" in DRC) have nillable="true" in WSDL)
+    CREATE TABLE bottlenecks (
+        bottleneck_id varchar PRIMARY KEY,
+        fk_g_fid isrs NOT NULL REFERENCES gauges,
+        -- XXX: DRC references "ch. 3.1.1", which does not exist in document.
+        objnam varchar,
+        nobjnm varchar,
+        stretch isrsrange NOT NULL,
+        area geometry(POLYGON, 4326) NOT NULL,
+        rb char(2) NOT NULL REFERENCES countries, -- from rb_lb in interface
+        lb char(2) NOT NULL REFERENCES countries, -- from rb_lb in interface
+        responsible_country char(2) NOT NULL REFERENCES countries,
+        revisiting_time smallint NOT NULL,
+        limiting varchar NOT NULL REFERENCES limiting_factors,
+        -- surtyp varchar NOT NULL REFERENCES survey_types,
+        -- XXX: Also an attribut of sounding result?
+        -- coverage varchar REFERENCES coverage_types,
+        -- XXX: Also an attribut of sounding result?
+        -- CHECK allowed combinations of surtyp and coverage or
+        -- different model approach?
+        -- depth_reference char(3) NOT NULL REFERENCES depth_references,
+        -- XXX: Also an attribut of sounding result?
+        date_info timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
+        source_organization varchar NOT NULL,
+        -- additional_data xml -- Currently not relevant for GEMMA
+        staging_done boolean NOT NULL DEFAULT false
+    )
+    CREATE TRIGGER bottleneck_date_info BEFORE UPDATE ON bottlenecks
+        FOR EACH ROW EXECUTE PROCEDURE update_date_info()
 
-CREATE TABLE gemma_fairway.effective_fairway_availability (
-    fairway_availability_id int NOT NULL REFERENCES fairway_availability,
-    measure_date timestamp with time zone NOT NULL,
-    level_of_service smallint NOT NULL REFERENCES levels_of_service,
-    PRIMARY KEY (fairway_availability_id, measure_date, level_of_service),
-    available_depth_value smallint,
-    available_width_value smallint,
-    water_level_value smallint,
-    CHECK(COALESCE(available_depth_value, available_width_value,
-        water_level_value) IS NOT NULL),
-    measure_type varchar NOT NULL REFERENCES measure_types,
-    source_organization varchar NOT NULL,
-    forecast_generation_time timestamp with time zone,
-    CHECK(measure_type <> 'forecasted'
-        OR forecast_generation_time IS NOT NULL),
-    value_lifetime timestamp with time zone,
-    CHECK(measure_type = 'minimum guaranteed' OR value_lifetime IS NOT NULL)
-);
+    CREATE TABLE bottlenecks_riverbed_materials (
+        bottleneck_id varchar REFERENCES bottlenecks,
+        riverbed varchar REFERENCES riverbed_materials,
+        -- XXX: should be 'natsur' according to IENC Encoding Guide M.4.3
+        PRIMARY KEY (bottleneck_id, riverbed)
+    )
+
+    CREATE TABLE sounding_results (
+        bottleneck_id varchar NOT NULL REFERENCES bottlenecks,
+        date_info date NOT NULL,
+        PRIMARY KEY (bottleneck_id, date_info),
+        area geometry(POLYGON, 4326) NOT NULL,
+        surtyp varchar NOT NULL REFERENCES survey_types,
+        coverage varchar REFERENCES coverage_types,
+        depth_reference char(3) NOT NULL REFERENCES depth_references,
+        sounding_data raster NOT NULL,
+        staging_done boolean NOT NULL DEFAULT false
+    )
+
+    --
+    -- Fairway availability
+    --
+    CREATE TABLE fairway_availability (
+        id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
+        position_code char(2) REFERENCES position_codes,
+        bottleneck_id varchar NOT NULL REFERENCES bottlenecks,
+        surdat date NOT NULL,
+        UNIQUE (bottleneck_id, surdat),
+        -- additional_data xml -- Currently not relevant for GEMMA
+        critical boolean,
+        date_info timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
+        source_organization varchar NOT NULL
+    )
+    CREATE TRIGGER fairway_availability_date_info
+        BEFORE UPDATE ON fairway_availability
+        FOR EACH ROW EXECUTE PROCEDURE update_date_info()
+
+    CREATE TABLE fa_reference_values (
+        fairway_availability_id int NOT NULL REFERENCES fairway_availability,
+        level_of_service smallint NOT NULL REFERENCES levels_of_service,
+        PRIMARY KEY (fairway_availability_id, level_of_service),
+        fairway_depth smallint,
+        fairway_width smallint,
+        fairway_radius int,
+        CHECK(COALESCE(fairway_depth, fairway_width, fairway_radius)
+            IS NOT NULL),
+        shallowest_spot geometry(POINT, 4326)
+    )
+
+    CREATE TABLE bottleneck_pdfs (
+        fairway_availability_id int NOT NULL REFERENCES fairway_availability,
+        profile_pdf_filename varchar NOT NULL, -- redundant to last part of URL
+        profile_pdf_url varchar NOT NULL, -- prohibit insecure URLs somehow?
+        PRIMARY KEY (fairway_availability_id, profile_pdf_url),
+        pdf_generation_date timestamp with time zone NOT NULL,
+        source_organization varchar NOT NULL
+    )
+
+    CREATE TABLE effective_fairway_availability (
+        fairway_availability_id int NOT NULL REFERENCES fairway_availability,
+        measure_date timestamp with time zone NOT NULL,
+        level_of_service smallint NOT NULL REFERENCES levels_of_service,
+        PRIMARY KEY (fairway_availability_id, measure_date, level_of_service),
+        available_depth_value smallint,
+        available_width_value smallint,
+        water_level_value smallint,
+        CHECK(COALESCE(available_depth_value, available_width_value,
+            water_level_value) IS NOT NULL),
+        measure_type varchar NOT NULL REFERENCES measure_types,
+        source_organization varchar NOT NULL,
+        forecast_generation_time timestamp with time zone,
+        CHECK(measure_type <> 'forecasted'
+            OR forecast_generation_time IS NOT NULL),
+        value_lifetime timestamp with time zone,
+        CHECK(measure_type = 'minimum guaranteed'
+            OR value_lifetime IS NOT NULL)
+    )
+;
 
 COMMIT;
--- a/schema/manage_users.sql	Fri Jul 20 18:32:40 2018 +0200
+++ b/schema/manage_users.sql	Fri Jul 20 17:28:16 2018 +0200
@@ -3,18 +3,18 @@
 -- exposing it to privileged users
 --
 
-CREATE OR REPLACE FUNCTION create_user(
+CREATE OR REPLACE FUNCTION sys_admin.create_user(
        userrole varchar,
-       username user_profiles.username%TYPE,
+       username users.user_profiles.username%TYPE,
        pw varchar,
-       country user_profiles.country%TYPE,
-       map_extent user_profiles.map_extent%TYPE,
-       email_adress user_profiles.email_adress%TYPE
+       country users.user_profiles.country%TYPE,
+       map_extent users.user_profiles.map_extent%TYPE,
+       email_adress users.user_profiles.email_adress%TYPE
     )
     RETURNS void
 AS $$
 BEGIN
-    INSERT INTO user_profiles VALUES (
+    INSERT INTO users.user_profiles VALUES (
         username, country, map_extent, email_adress);
     EXECUTE format(
         'CREATE ROLE %I IN ROLE %I LOGIN PASSWORD %L', username, userrole, pw);
--- a/schema/manage_users_tests.sql	Fri Jul 20 18:32:40 2018 +0200
+++ b/schema/manage_users_tests.sql	Fri Jul 20 17:28:16 2018 +0200
@@ -2,30 +2,36 @@
 --
 -- pgTAP test script for user management functions
 --
-SELECT plan(5); -- Give number of tests that have to be run
+SELECT plan(6); -- Give number of tests that have to be run
 
 SET search_path TO public, gemma, gemma_waterway, gemma_fairway;
 
---
--- Run tests as system_admin
---
+SET SESSION AUTHORIZATION waterway_admin;
+
+SELECT throws_ok($$
+    SELECT sys_admin.create_user(
+        'waterway_user', 'test0', 'secret', 'AT', NULL, 'test0')
+    $$,
+    42501, NULL,
+    'Less privileged user cannot call function in schema sys_admin');
+
 SET SESSION AUTHORIZATION sys_admin;
 
 SELECT lives_ok($$
-    SELECT create_user(
+    SELECT sys_admin.create_user(
         'waterway_user', 'test1', 'secret', 'AT', NULL, 'test1')
     $$,
     'New waterway user can be added');
 
 SELECT throws_ok($$
-    SELECT create_user(
+    SELECT sys_admin.create_user(
         'invalid', 'test2', 'secret', 'AT', NULL, 'test2')
     $$,
     42704, NULL,
     'Valid role name has to be provided');
 
 SELECT throws_ok($$
-    SELECT create_user(
+    SELECT sys_admin.create_user(
         'waterway_user', NULL, 'secret', 'AT', NULL, 'test3')
     $$,
     23502, NULL,
@@ -33,14 +39,14 @@
 -- Though other arguments are mandatory, too, there are no explicit tests
 
 SELECT throws_ok($$
-    SELECT create_user(
+    SELECT sys_admin.create_user(
         'waterway_user', 'waterway_user', 'secret', 'AT', NULL, 'test4')
     $$,
     23505, NULL,
     'No duplicate user name is allowed');
 
 SELECT throws_ok($$
-    SELECT create_user(
+    SELECT sys_admin.create_user(
         'waterway_user', 'test2', 'secret', 'AT', NULL, 'xxx')
     $$,
     23505, NULL,
--- a/schema/tap_tests_data.sql	Fri Jul 20 18:32:40 2018 +0200
+++ b/schema/tap_tests_data.sql	Fri Jul 20 17:28:16 2018 +0200
@@ -1,11 +1,13 @@
-SET search_path TO public, gemma, gemma_waterway, gemma_fairway;
+--
+-- Test data used in *_tests.sql scripts
+--
 
 INSERT INTO countries VALUES ('AT'), ('RO');
 
-INSERT INTO responsibility_areas VALUES
+INSERT INTO users.responsibility_areas VALUES
     ('AT', ST_geomfromtext('MULTIPOLYGON(((0 0, 0 1, 1 1, 1 0, 0 0)))', 4326));
 
-INSERT INTO user_profiles (username, country, email_adress)
+INSERT INTO users.user_profiles (username, country, email_adress)
     VALUES
     ('waterway_user', 'AT', 'xxx'),
     ('waterway_user2', 'RO', 'xxy'),
@@ -14,7 +16,7 @@
 
 INSERT INTO limiting_factors VALUES ('depth'), ('width');
 
-INSERT INTO gauges (
+INSERT INTO waterway.gauges (
     location, function_code, objname, geom, zero_point, source_organization)
     VALUES (
         ('AT', 'XXX', '00001', '00000', 1)::isrs,
@@ -25,7 +27,7 @@
         'testorganization'
     );
 
-INSERT INTO bottlenecks (
+INSERT INTO waterway.bottlenecks (
     bottleneck_id, fk_g_fid, stretch, area, rb, lb, responsible_country,
     revisiting_time, limiting, source_organization, staging_done)
     VALUES (
@@ -46,7 +48,7 @@
         1, 'depth', 'testorganization', true
     );
 
-INSERT INTO templates (template_name, template_data)
+INSERT INTO users.templates (template_name, template_data)
     VALUES ('AT', '\x'), ('RO', '\x');
-INSERT INTO user_templates
+INSERT INTO users.user_templates
     VALUES ('waterway_user', 'AT'), ('waterway_user2', 'RO');