diff --git a/pyproject.toml b/pyproject.toml index 9f31cacc..1b11596b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,4 +90,4 @@ max-complexity = 20 # TODO: lower this [tool.ruff.lint.per-file-ignores] "**/migrations/*.py" = ["E501"] # line too long -"tests/**/*.py" = ["S101", "S105", "S106", "S314", "S608"] # allow asserts, hardcoded passwords, SQL injection +"tests/**/*.py" = ["E501", "S101", "S105", "S106", "S314", "S608"] # allow long lines, asserts, hardcoded passwords, SQL injection diff --git a/src/schematools/permissions/db.py b/src/schematools/permissions/db.py index 7cf80b69..3404f838 100644 --- a/src/schematools/permissions/db.py +++ b/src/schematools/permissions/db.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from collections import defaultdict +from dataclasses import dataclass from typing import Any, cast from pg_grant import PgObjectType, parse_acl_item, query @@ -19,7 +19,8 @@ # configure the logger, if needed. logger = logging.getLogger(__name__) -existing_roles = set() +existing_roles = set() # note: used as global cache! +existing_sequences = {} # def is_remote(table_name: str) -> bool: @@ -173,15 +174,16 @@ def set_dataset_write_permissions( grantee = f"write_{ams_schema.db_name}" if create_roles: _create_role_if_not_exists(session, grantee, dry_run=dry_run) + for table in ams_schema.get_tables(include_nested=True, include_through=True): table_name = table.db_name if is_remote(table_name): continue - table_privileges = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES"] + _execute_grant( session, grant( - table_privileges, + ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES"], PgObjectType.TABLE, table_name, grantee, @@ -191,13 +193,40 @@ def set_dataset_write_permissions( echo=echo, dry_run=dry_run, ) + if table.is_autoincrement: + # Get PostgreSQL generated sequence for 'id' column that Django added. + sequence_name = _get_sequence_name(session, table_name, "id") + _execute_grant( + session, + grant( + ["USAGE"], + PgObjectType.SEQUENCE, + sequence_name, + grantee, + grant_option=False, + schema=pg_schema, + ), + echo=echo, + dry_run=dry_run, + ) + + +@dataclass +class GrantParam: + """Which object to give which permission. + This intermediate object is used to collect results before making + statements like ``GRANT SELECT ON target TO ``. + """ + + privileges: list[str] + target_type: PgObjectType # often PgObjectType.TABLE + target: str + grantees: list[str] def get_all_dataset_scopes( - ams_schema: DatasetSchema, - role: str, - scope: str, -) -> defaultdict[str, list]: + session, ams_schema: DatasetSchema, role: str, scope: str +) -> list[GrantParam]: """Returns all scopes that should be applied to the tables of a dataset. Args: @@ -222,39 +251,24 @@ def get_all_dataset_scopes( the associated sub-table gets the grant `scope_bar`. Returns: - all_scopes (defaultdict): Contains for each table in the dataset a list of scopes with - priviliges and grants: - - '"table1":[ - { - "privileges": ["SELECT"], - "grantees": ["scope_openbaar"]), - } - ], - "table2": - [ - { - "privileges": ["SELECT columnA"], - "grantees": ["scope_openbaar"]), - }, - { - "privileges": ["SELECT columnB"], - "grantees": ["scope_A", "scope_B"]), - } - ] - ' + all_scopes (list): Contains a list of scopes with priviliges and grants: + + [ + GrantScope(["SELECT"], PgObjectType.TABLE, "table1", ["scope_openbaar"]), + GrantScope(["SELECT (columnA)"], PgObjectType.TABLE, "table2", ["scope_openbaar"]), + GrantScope(["SELECT (columnB)"], PgObjectType.TABLE, "table2", ["scope_A", "scope_B"]), + ] """ def _fetch_grantees(scopes: frozenset[str]) -> list[str]: if role == "AUTO": - grantees = [scope_to_role(scope) for scope in scopes] + return [scope_to_role(_scope) for _scope in scopes] elif scope in scopes: - grantees = [role] + return [role] else: - grantees = [] - return grantees + return [] - all_scopes = defaultdict(list) + all_scopes = [] dataset_scopes = ams_schema.auth for table in ams_schema.get_tables(include_nested=True, include_through=True): @@ -262,44 +276,24 @@ def _fetch_grantees(scopes: frozenset[str]) -> list[str]: if is_remote(table_name): continue - table_scopes = table.auth - fallback_scope = (table_scopes - {PUBLIC_SCOPE}) or dataset_scopes + table_scopes = (table.auth - {PUBLIC_SCOPE}) or dataset_scopes fields = [ - field for field in table.get_fields(include_subfields=True) if field.name != "schema" + field + for field in table.get_fields(include_subfields=True) + if not field.type.endswith("#/definitions/schema") ] - column_scopes = {} - # First process all fields, to know if any fields has a non-public scope + column_scopes = {} for field in fields: - column_name = field.db_name # Object type relations have subfields, in that case # the auth scope on the relation is leading. - parent_field_scopes: frozenset[str] = frozenset() - if field.is_subfield: - parent_field_scopes = field.parent_field.auth - {PUBLIC_SCOPE} - field_scopes = field.auth - {PUBLIC_SCOPE} - final_scopes: frozenset[str] = parent_field_scopes or field_scopes + if field.is_subfield: + field_scopes = (field.parent_field.auth - {PUBLIC_SCOPE}) or field_scopes - if final_scopes: - column_scopes[column_name] = final_scopes - - if field.is_nested_table: - all_scopes[field.nested_table.db_name].append( - { - "privileges": ["SELECT"], - "grantees": _fetch_grantees(final_scopes or fallback_scope), - } - ) - - if field.nm_relation is not None: - all_scopes[field.through_table.db_name].append( - { - "privileges": ["SELECT"], - "grantees": _fetch_grantees(final_scopes or fallback_scope), - } - ) + if field_scopes: + column_scopes[field.db_name] = field_scopes if column_scopes: for field in fields: @@ -308,23 +302,49 @@ def _fetch_grantees(scopes: frozenset[str]) -> list[str]: continue column_name = field.db_name - all_scopes[table_name].append( - # NB. space after SELECT is significant! - { - "privileges": [f"SELECT ({column_name})"], - "grantees": _fetch_grantees( - column_scopes.get(column_name, fallback_scope) - ), - } + grantees = _fetch_grantees(column_scopes.get(column_name, table_scopes)) + all_scopes.append( + GrantParam( + # NB. space after SELECT is significant! + privileges=[f"SELECT ({column_name})"], + target_type=PgObjectType.TABLE, + target=table_name, + grantees=grantees, + ) ) + if field.is_primary and table.is_autoincrement: + # Get PostgreSQL generated sequence for 'id' column that Django added. + sequence_name = _get_sequence_name(session, table_name, "id") + all_scopes.append( + GrantParam( + privileges=["SELECT"], + target_type=PgObjectType.SEQUENCE, + target=sequence_name, + grantees=grantees, + ) + ) else: if table_name not in all_scopes: - all_scopes[table_name].append( - { - "privileges": ["SELECT"], - "grantees": _fetch_grantees(fallback_scope), - } + grantees = _fetch_grantees(table_scopes) + all_scopes.append( + GrantParam( + privileges=["SELECT"], + target_type=PgObjectType.TABLE, + target=table_name, + grantees=grantees, + ) ) + if table.is_autoincrement: + sequence_name = _get_sequence_name(session, table_name, "id") + all_scopes.append( + GrantParam( + privileges=["SELECT"], + target_type=PgObjectType.SEQUENCE, + target=sequence_name, + grantees=grantees, + ) + ) + return all_scopes @@ -366,32 +386,29 @@ def set_dataset_read_permissions( If NM and nested relation fields (type `array` in the schema) have a scope `bar` the associated sub-table gets the grant `scope_bar`. """ - grantee: str | None = f"write_{ams_schema.db_name}" - grantee = None if role == "AUTO" else role if create_roles and grantee: - _create_role_if_not_exists(session, grantee) - - all_scopes = get_all_dataset_scopes(ams_schema, role, scope) - - for table_name, grant_params in all_scopes.items(): - for grant_param in grant_params: - for _grantee in grant_param["grantees"]: - if create_roles: - _create_role_if_not_exists(session, _grantee, dry_run=dry_run) - _execute_grant( - session, - grant( - grant_param["privileges"], - PgObjectType.TABLE, - table_name, - _grantee, - grant_option=False, - schema=pg_schema, - ), - echo=echo, - dry_run=dry_run, - ) + _create_role_if_not_exists(session, grantee, dry_run=dry_run) + + all_grants = get_all_dataset_scopes(session, ams_schema, role, scope) + for grant_param in all_grants: + # For global and specific columns: + for _grantee in grant_param.grantees: + if create_roles: + _create_role_if_not_exists(session, _grantee, dry_run=dry_run) + _execute_grant( + session, + grant( + grant_param.privileges, + type=grant_param.target_type, + target=grant_param.target, + grantee=_grantee, + grant_option=False, + schema=pg_schema, + ), + echo=echo, + dry_run=dry_run, + ) def set_additional_grants( @@ -463,57 +480,37 @@ def create_acl_from_schemas( Revoke old privileges before assigning new in case new privileges are more restrictive. """ if revoke: + # For a single dataset, or all tables? + revoke_dataset = schemas if isinstance(schemas, DatasetSchema) else None if role == "AUTO": - if isinstance(schemas, DatasetSchema): - # for a single dataset - _revoke_all_privileges_from_read_and_write_roles( - session, pg_schema, schemas, dry_run=dry_run, echo=bool(verbose) - ) - else: - _revoke_all_privileges_from_read_and_write_roles( - session, pg_schema, dry_run=dry_run, echo=bool(verbose) - ) + # All roles + _revoke_all_privileges_from_read_and_write_roles( + session, pg_schema, revoke_dataset, dry_run=dry_run, echo=bool(verbose) + ) else: - if isinstance(schemas, DatasetSchema): - # for a single dataset - _revoke_all_privileges_from_role( - session, pg_schema, role, schemas, dry_run=dry_run, echo=bool(verbose) - ) - else: - _revoke_all_privileges_from_role( - session, pg_schema, role, dry_run=dry_run, echo=bool(verbose) - ) + # Only for a single role + _revoke_all_privileges_from_role( + session, pg_schema, role, revoke_dataset, dry_run=dry_run, echo=bool(verbose) + ) - if set_read_permissions: - if isinstance(schemas, DatasetSchema): - # for a single dataset + datasets = [schemas] if isinstance(schemas, DatasetSchema) else schemas.values() + for dataset in datasets: + if set_read_permissions: set_dataset_read_permissions( - session, pg_schema, schemas, role, scope, dry_run, create_roles, echo=bool(verbose) + session, + pg_schema, + dataset, + role, + scope, + dry_run, + create_roles, + echo=bool(verbose), ) - else: - for dataset_schema in schemas.values(): - set_dataset_read_permissions( - session, - pg_schema, - dataset_schema, - role, - scope, - dry_run, - create_roles, - echo=bool(verbose), - ) - if set_write_permissions: - if isinstance(schemas, DatasetSchema): - # for a single dataset + if set_write_permissions: set_dataset_write_permissions( - session, pg_schema, schemas, dry_run, create_roles, echo=bool(verbose) + session, pg_schema, dataset, dry_run, create_roles, echo=bool(verbose) ) - else: - for dataset_schema in schemas.values(): - set_dataset_write_permissions( - session, pg_schema, dataset_schema, dry_run, create_roles, echo=bool(verbose) - ) def _revoke_all_privileges_from_role( @@ -584,11 +581,19 @@ def _revoke_all_privileges_from_read_and_write_roles( revoke_statements.append( f"REVOKE ALL PRIVILEGES ON {pg_schema}.{table.db_name} FROM {rolname[0]}" ) - revoke_statement = ";".join(revoke_statements) + if table.is_autoincrement: + sequence_name = _get_sequence_name(session, table.db_name, "id") + revoke_statements.append( + f"REVOKE ALL PRIVILEGES ON SEQUENCE {pg_schema}.{sequence_name}" + f" FROM {rolname[0]}" + ) else: - revoke_statement = ( - f"REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA {pg_schema} FROM {rolname[0]}" - ) + revoke_statements = [ + f"REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA {pg_schema} FROM {rolname[0]}", + f"REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA {pg_schema} FROM {rolname[0]}", + ] + + revoke_statement = ";".join(revoke_statements) revoke_block_statement = f""" DO $$ @@ -658,6 +663,21 @@ def _create_role_if_not_exists( existing_roles.add(role) +def _get_sequence_name(session: Session, table_name: str, column: str) -> str | None: + key = (table_name, column) + try: + # Can't use lru_cache() as it caches 'session' too. + return existing_sequences[key] + except KeyError: + row = session.execute( + text("SELECT pg_get_serial_sequence(:table, :column)"), + {"table": table_name, "column": column}, + ).first() + value = row[0].replace("public.", "").replace('"', "") if row is not None else None + existing_sequences[key] = value + return value + + def scope_to_role(scope: str) -> str: """Return rolename for the postgres database.""" return f"scope_{scope.lower().replace('/', '_')}" diff --git a/src/schematools/types.py b/src/schematools/types.py index b8a0fe11..d9ecaf62 100644 --- a/src/schematools/types.py +++ b/src/schematools/types.py @@ -548,7 +548,7 @@ def build_nested_table(self, field: DatasetFieldSchema) -> DatasetTableSchema: "originalID": field.id, "type": "table", "version": str(table.version), - "auth": list(table.auth), + "auth": list((field.auth - {_PUBLIC_SCOPE}) or (table.auth - {_PUBLIC_SCOPE})) or None, "description": f"Auto-generated table for nested field: {table.id}.{field.id}", "schema": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -561,7 +561,6 @@ def build_nested_table(self, field: DatasetFieldSchema) -> DatasetTableSchema: "parent": { "type": parent_fk_type, "relation": f"{self.id}:{table.id}", - "auth": list(field.auth), }, **field["items"]["properties"], }, @@ -638,6 +637,8 @@ def build_through_table(self, field: DatasetFieldSchema) -> DatasetTableSchema: "id": f"{table.id}_{target_field_id}", "type": "table", "version": str(table.version), + "auth": list((field.auth - {_PUBLIC_SCOPE}) or (field.table.auth - {_PUBLIC_SCOPE})) + or None, "originalID": field.id, "throughFields": [left_table_id, target_field_id], "description": f"Auto-generated M2M table for {table.id}.{field.id}", diff --git a/tests/files/datasets/brk.json b/tests/files/datasets/brk.json index 4c3da79b..a7046521 100644 --- a/tests/files/datasets/brk.json +++ b/tests/files/datasets/brk.json @@ -173,6 +173,7 @@ "description": "Als dit veld is gevuld geeft dit de omschrijving waarom dit gegeven in onderzoek staat (art. 7n en 7r Kadasterwet)." }, "heeftEenRelatieMetVerblijfsobject": { + "shortname": "hftRelMtVot", "type": "array", "items": { "type": "object", @@ -880,6 +881,7 @@ "description": "Identificatie van het betrokken subject" }, "heeftBetrekkingOpKadastraalObject": { + "shortname": "hftBtrkOpKot", "type": "object", "properties": { "identificatie": { diff --git a/tests/files/datasets/brk_without_bag_relations.json b/tests/files/datasets/brk_without_bag_relations.json index 83541e7f..2832a138 100644 --- a/tests/files/datasets/brk_without_bag_relations.json +++ b/tests/files/datasets/brk_without_bag_relations.json @@ -173,6 +173,7 @@ "description": "Als dit veld is gevuld geeft dit de omschrijving waarom dit gegeven in onderzoek staat (art. 7n en 7r Kadasterwet)." }, "heeftEenRelatieMetVerblijfsobject": { + "shortname": "hftRelMtVot", "type": "array", "items": { "type": "object", @@ -865,6 +866,7 @@ "description": "Identificatie van het betrokken subject" }, "heeftBetrekkingOpKadastraalObject": { + "shortname": "hftBtrkOpKot", "type": "object", "properties": { "identificatie": { @@ -878,6 +880,7 @@ "description": "Identificatie van het kadastrale object (onroerende zaak)" }, "isGebaseerdOpStukdeel": { + "shortname": "isGbsdOpSdl", "type": "string", "relation": "brk:stukdelen", "description": "Identificatie van het betrokken stukdeel" diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 84441412..46f724fd 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import logging import pytest from psycopg2.errors import DuplicateObject @@ -43,7 +44,9 @@ def test_auto_permissions(self, here, engine, gebieden_schema_auth, dbsession): engine, "scope_level_c", "gebieden_bouwblokken", "begin_geldigheid" ) - def test_nm_relations_permissions(self, here, engine, kadastraleobjecten_schema, dbsession): + def test_nm_relations_permissions( + self, here, engine, kadastraleobjecten_schema, dbsession, caplog + ): importer = NDJSONImporter(kadastraleobjecten_schema, engine) importer.generate_db_objects("kadastraleobjecten", truncate=True, ind_extra_index=False) @@ -63,13 +66,49 @@ def test_nm_relations_permissions(self, here, engine, kadastraleobjecten_schema, # make sure role 'write_brk' exists with create_roles=True # The role exists now for all test following this statement - apply_schema_and_profile_permissions( - engine, "public", ams_schema, {}, "openbaar", "OPENBAAR", create_roles=True - ) - apply_schema_and_profile_permissions( - engine, "public", ams_schema, {}, "brk_rsn", "BRK/RSN" - ) - apply_schema_and_profile_permissions(engine, "public", ams_schema, {}, "brk_ro", "BRK/RO") + with caplog.at_level(logging.INFO, logger="schematools.permissions.db"): + apply_schema_and_profile_permissions( + engine, + "public", + ams_schema, + {}, + "openbaar", + "OPENBAAR", + create_roles=True, + verbose=1, + ) + apply_schema_and_profile_permissions( + engine, "public", ams_schema, {}, "brk_rsn", "BRK/RSN", verbose=1 + ) + apply_schema_and_profile_permissions( + engine, "public", ams_schema, {}, "brk_ro", "BRK/RO", verbose=1 + ) + + grants = _filter_grant_statements(caplog) + assert grants == [ + "GRANT SELECT (begin_geldigheid) ON TABLE public.brk_kadastraleobjecten TO brk_rsn", + "GRANT SELECT (eind_geldigheid) ON TABLE public.brk_kadastraleobjecten TO brk_rsn", + "GRANT SELECT (id) ON TABLE public.brk_kadastraleobjecten TO brk_rsn", + "GRANT SELECT (identificatie) ON TABLE public.brk_kadastraleobjecten TO brk_rsn", + "GRANT SELECT (koopsom) ON TABLE public.brk_kadastraleobjecten TO brk_ro", + "GRANT SELECT (neuron_id) ON TABLE public.brk_kadastraleobjecten TO brk_rsn", + "GRANT SELECT (registratiedatum) ON TABLE public.brk_kadastraleobjecten TO brk_rsn", + "GRANT SELECT (soort_cultuur_onbebouwd_code) ON TABLE public.brk_kadastraleobjecten TO brk_ro", + "GRANT SELECT (soort_cultuur_onbebouwd_omschrijving) ON TABLE public.brk_kadastraleobjecten TO brk_ro", + "GRANT SELECT (soort_grootte) ON TABLE public.brk_kadastraleobjecten TO brk_rsn", + "GRANT SELECT (volgnummer) ON TABLE public.brk_kadastraleobjecten TO brk_rsn", + "GRANT SELECT ON SEQUENCE public.brk_kadastraleobjecten_is_ontstaan_uit_kadastraalobject_id_seq TO brk_rsn", + "GRANT SELECT ON TABLE public.brk_kadastraleobjecten_is_ontstaan_uit_kadastraalobject TO brk_rsn", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_kadastraleobjecten TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_kadastraleobjecten TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_kadastraleobjecten TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_kadastraleobjecten_is_ontstaan_uit_kadastraalobject TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_kadastraleobjecten_is_ontstaan_uit_kadastraalobject TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_kadastraleobjecten_is_ontstaan_uit_kadastraalobject TO write_brk", + "GRANT USAGE ON SEQUENCE public.brk_kadastraleobjecten_is_ontstaan_uit_kadastraalobject_id_seq TO write_brk", + "GRANT USAGE ON SEQUENCE public.brk_kadastraleobjecten_is_ontstaan_uit_kadastraalobject_id_seq TO write_brk", + "GRANT USAGE ON SEQUENCE public.brk_kadastraleobjecten_is_ontstaan_uit_kadastraalobject_id_seq TO write_brk", + ] # table denied _check_select_permission_denied(engine, "openbaar", "brk_kadastraleobjecten") @@ -97,7 +136,95 @@ def test_nm_relations_permissions(self, here, engine, kadastraleobjecten_schema, engine, "brk_rsn", "brk_kadastraleobjecten_is_ontstaan_uit_kadastraalobject" ) - def test_openbaar_permissions(self, here, engine, afval_schema, dbsession): + def test_brk_permissions( + self, here, engine, brk_schema_without_bag_relations, dbsession, caplog + ): + """Prove that a dataset with many nested tables get the proper permissions.""" + importer = NDJSONImporter(brk_schema_without_bag_relations, engine) + for table in brk_schema_without_bag_relations.get_tables(): + importer.generate_db_objects(table.id, truncate=True, ind_extra_index=False) + + # Setup schema and profile + # This schema has auth on dataset level, not on table + ams_schema = {brk_schema_without_bag_relations.id: brk_schema_without_bag_relations} + + # make sure role 'write_brk' exists with create_roles=True + # The role exists now for all test following this statement + with caplog.at_level(logging.INFO, logger="schematools.permissions.db"): + apply_schema_and_profile_permissions( + engine, + "public", + ams_schema, + {}, + "AUTO", + "ALL", + create_roles=True, + verbose=1, + ) + + grants = _filter_grant_statements(caplog) + assert grants == [ + "GRANT SELECT ON SEQUENCE public.brk_aantekeningenkadastraleobjecten_heeft_betrokken_pers_id_seq TO scope_brk_rsn", + "GRANT SELECT ON SEQUENCE public.brk_aantekeningenrechten_heeft_betrokken_persoon_id_seq TO scope_brk_rsn", + "GRANT SELECT ON SEQUENCE public.brk_aantekeningenrechten_is_gbsd_op_sdl_id_seq TO scope_brk_rsn", + "GRANT SELECT ON SEQUENCE public.brk_kadastraleobjecten_hft_rel_mt_vot_id_seq TO scope_brk_rsn", + "GRANT SELECT ON SEQUENCE public.brk_kadastraleobjecten_soort_cultuur_bebouwd_id_seq TO scope_brk_rsn", + "GRANT SELECT ON SEQUENCE public.brk_stukdelen_is_bron_voor_aantekening_kadastraal_object_id_seq TO scope_brk_rsn", + "GRANT SELECT ON SEQUENCE public.brk_stukdelen_is_bron_voor_aantekening_recht_id_seq TO scope_brk_rsn", + "GRANT SELECT ON SEQUENCE public.brk_stukdelen_is_bron_voor_zakelijk_recht_id_seq TO scope_brk_rsn", + "GRANT SELECT ON TABLE public.brk_aantekeningenkadastraleobjecten TO scope_brk_rsn", + "GRANT SELECT ON TABLE public.brk_aantekeningenkadastraleobjecten_heeft_betrokken_persoon TO scope_brk_rsn", + "GRANT SELECT ON TABLE public.brk_aantekeningenrechten TO scope_brk_rsn", + "GRANT SELECT ON TABLE public.brk_aantekeningenrechten_heeft_betrokken_persoon TO scope_brk_rsn", + "GRANT SELECT ON TABLE public.brk_aantekeningenrechten_is_gbsd_op_sdl TO scope_brk_rsn", + "GRANT SELECT ON TABLE public.brk_aardzakelijkerechten TO scope_brk_rsn", + "GRANT SELECT ON TABLE public.brk_gemeentes TO scope_brk_rsn", + "GRANT SELECT ON TABLE public.brk_kadastralegemeentecodes TO scope_brk_rsn", + "GRANT SELECT ON TABLE public.brk_kadastralegemeentes TO scope_brk_rsn", + "GRANT SELECT ON TABLE public.brk_kadastraleobjecten TO scope_brk_rsn", + "GRANT SELECT ON TABLE public.brk_kadastraleobjecten_hft_rel_mt_vot TO scope_brk_rsn", + "GRANT SELECT ON TABLE public.brk_kadastraleobjecten_soort_cultuur_bebouwd TO scope_brk_rsn", + "GRANT SELECT ON TABLE public.brk_kadastralesecties TO scope_brk_rsn", + "GRANT SELECT ON TABLE public.brk_kadastralesubjecten TO scope_brk_rsn", + "GRANT SELECT ON TABLE public.brk_meta TO scope_brk_rsn", + "GRANT SELECT ON TABLE public.brk_stukdelen TO scope_brk_rsn", + "GRANT SELECT ON TABLE public.brk_stukdelen_is_bron_voor_aantekening_kadastraal_object TO scope_brk_rsn", + "GRANT SELECT ON TABLE public.brk_stukdelen_is_bron_voor_aantekening_recht TO scope_brk_rsn", + "GRANT SELECT ON TABLE public.brk_stukdelen_is_bron_voor_zakelijk_recht TO scope_brk_rsn", + "GRANT SELECT ON TABLE public.brk_tenaamstellingen TO scope_brk_rsn", + "GRANT SELECT ON TABLE public.brk_zakelijkerechten TO scope_brk_rsn", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_aantekeningenkadastraleobjecten TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_aantekeningenkadastraleobjecten_heeft_betrokken_persoon TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_aantekeningenrechten TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_aantekeningenrechten_heeft_betrokken_persoon TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_aantekeningenrechten_is_gbsd_op_sdl TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_aardzakelijkerechten TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_gemeentes TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_kadastralegemeentecodes TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_kadastralegemeentes TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_kadastraleobjecten TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_kadastraleobjecten_hft_rel_mt_vot TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_kadastraleobjecten_soort_cultuur_bebouwd TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_kadastralesecties TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_kadastralesubjecten TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_meta TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_stukdelen TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_stukdelen_is_bron_voor_aantekening_kadastraal_object TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_stukdelen_is_bron_voor_aantekening_recht TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_stukdelen_is_bron_voor_zakelijk_recht TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_tenaamstellingen TO write_brk", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.brk_zakelijkerechten TO write_brk", + "GRANT USAGE ON SEQUENCE public.brk_aantekeningenkadastraleobjecten_heeft_betrokken_pers_id_seq TO write_brk", + "GRANT USAGE ON SEQUENCE public.brk_aantekeningenrechten_heeft_betrokken_persoon_id_seq TO write_brk", + "GRANT USAGE ON SEQUENCE public.brk_aantekeningenrechten_is_gbsd_op_sdl_id_seq TO write_brk", + "GRANT USAGE ON SEQUENCE public.brk_kadastraleobjecten_hft_rel_mt_vot_id_seq TO write_brk", + "GRANT USAGE ON SEQUENCE public.brk_kadastraleobjecten_soort_cultuur_bebouwd_id_seq TO write_brk", + "GRANT USAGE ON SEQUENCE public.brk_stukdelen_is_bron_voor_aantekening_kadastraal_object_id_seq TO write_brk", + "GRANT USAGE ON SEQUENCE public.brk_stukdelen_is_bron_voor_aantekening_recht_id_seq TO write_brk", + "GRANT USAGE ON SEQUENCE public.brk_stukdelen_is_bron_voor_zakelijk_recht_id_seq TO write_brk", + ] + + def test_openbaar_permissions(self, here, engine, afval_schema, dbsession, caplog): """ Prove that the default auth scope is "OPENBAAR". """ @@ -121,24 +248,33 @@ def test_openbaar_permissions(self, here, engine, afval_schema, dbsession): _check_select_permission_denied(engine, "openbaar", "afvalwegingen_containers") _check_select_permission_denied(engine, "bag_r", "afvalwegingen_clusters") - apply_schema_and_profile_permissions( - engine=engine, - pg_schema="public", - ams_schema=ams_schema, - profiles=profiles, - role="openbaar", - scope="OPENBAAR", - create_roles=True, - ) - apply_schema_and_profile_permissions( - engine=engine, - pg_schema="public", - ams_schema=ams_schema, - profiles=profiles, - role="bag_r", - scope="BAG/R", - create_roles=True, - ) + with caplog.at_level(logging.INFO, logger="schematools.permissions.db"): + apply_schema_and_profile_permissions( + engine=engine, + pg_schema="public", + ams_schema=ams_schema, + profiles=profiles, + role="openbaar", + scope="OPENBAAR", + create_roles=True, + ) + apply_schema_and_profile_permissions( + engine=engine, + pg_schema="public", + ams_schema=ams_schema, + profiles=profiles, + role="bag_r", + scope="BAG/R", + create_roles=True, + verbose=1, + ) + + grants = _filter_grant_statements(caplog) + assert grants == [ + "GRANT SELECT ON TABLE public.afvalwegingen_clusters TO bag_r", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.afvalwegingen_clusters TO write_afvalwegingen", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.afvalwegingen_containers TO write_afvalwegingen", + ] _check_select_permission_granted(engine, "openbaar", "afvalwegingen_containers") _check_select_permission_denied(engine, "openbaar", "afvalwegingen_clusters") @@ -212,7 +348,9 @@ def test_interacting_permissions(self, here, engine, gebieden_schema_auth, dbses ) _check_select_permission_denied(engine, "level_c", "gebieden_buurten") - def test_auth_list_permissions(self, here, engine, gebieden_schema_auth_list, dbsession): + def test_auth_list_permissions( + self, here, engine, gebieden_schema_auth_list, dbsession, caplog + ): """ Prove that dataset, table, and field permissions are set, according to the "OF-OF" Exclusief principle. @@ -257,24 +395,94 @@ def test_auth_list_permissions(self, here, engine, gebieden_schema_auth_list, db _check_select_permission_denied(engine, test_role, table) # Apply the permissions from Schema and Profiles. - apply_schema_and_profile_permissions( - engine, "public", ams_schema, profiles, "level_a1", "LEVEL/A1" - ) - apply_schema_and_profile_permissions( - engine, "public", ams_schema, profiles, "level_b1", "LEVEL/B1" - ) - apply_schema_and_profile_permissions( - engine, "public", ams_schema, profiles, "level_c1", "LEVEL/C1" - ) - apply_schema_and_profile_permissions( - engine, "public", ams_schema, profiles, "level_a2", "LEVEL/A2" - ) - apply_schema_and_profile_permissions( - engine, "public", ams_schema, profiles, "level_b2", "LEVEL/B2" - ) - apply_schema_and_profile_permissions( - engine, "public", ams_schema, profiles, "level_c2", "LEVEL/C2" - ) + with caplog.at_level(logging.INFO, logger="schematools.permissions.db"): + apply_schema_and_profile_permissions( + engine, "public", ams_schema, profiles, "level_a1", "LEVEL/A1", verbose=1 + ) + grants = _filter_grant_statements(caplog) + assert grants == [ + "GRANT SELECT ON SEQUENCE public.gebieden_buurten_ligt_in_wijk_id_seq TO level_a1", + "GRANT SELECT ON TABLE public.gebieden_buurten TO level_a1", + "GRANT SELECT ON TABLE public.gebieden_buurten_ligt_in_wijk TO level_a1", + "GRANT SELECT ON TABLE public.gebieden_wijken TO level_a1", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_bouwblokken TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_buurten TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_buurten_ligt_in_wijk TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_wijken TO write_gebieden", + "GRANT USAGE ON SEQUENCE public.gebieden_buurten_ligt_in_wijk_id_seq TO write_gebieden", + ] + + apply_schema_and_profile_permissions( + engine, "public", ams_schema, profiles, "level_b1", "LEVEL/B1", verbose=1 + ) + grants = _filter_grant_statements(caplog) + assert grants == [ + "GRANT SELECT (eind_geldigheid) ON TABLE public.gebieden_bouwblokken TO level_b1", + "GRANT SELECT (id) ON TABLE public.gebieden_bouwblokken TO level_b1", + "GRANT SELECT (ligt_in_buurt_id) ON TABLE public.gebieden_bouwblokken TO level_b1", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_bouwblokken TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_buurten TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_buurten_ligt_in_wijk TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_wijken TO write_gebieden", + "GRANT USAGE ON SEQUENCE public.gebieden_buurten_ligt_in_wijk_id_seq TO write_gebieden", + ] + + apply_schema_and_profile_permissions( + engine, "public", ams_schema, profiles, "level_c1", "LEVEL/C1", verbose=1 + ) + grants = _filter_grant_statements(caplog) + assert grants == [ + "GRANT SELECT (begin_geldigheid) ON TABLE public.gebieden_bouwblokken TO level_c1", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_bouwblokken TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_buurten TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_buurten_ligt_in_wijk TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_wijken TO write_gebieden", + "GRANT USAGE ON SEQUENCE public.gebieden_buurten_ligt_in_wijk_id_seq TO write_gebieden", + ] + + apply_schema_and_profile_permissions( + engine, "public", ams_schema, profiles, "level_a2", "LEVEL/A2", verbose=1 + ) + grants = _filter_grant_statements(caplog) + assert grants == [ + "GRANT SELECT ON SEQUENCE public.gebieden_buurten_ligt_in_wijk_id_seq TO level_a2", + "GRANT SELECT ON TABLE public.gebieden_buurten TO level_a2", + "GRANT SELECT ON TABLE public.gebieden_buurten_ligt_in_wijk TO level_a2", + "GRANT SELECT ON TABLE public.gebieden_wijken TO level_a2", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_bouwblokken TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_buurten TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_buurten_ligt_in_wijk TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_wijken TO write_gebieden", + "GRANT USAGE ON SEQUENCE public.gebieden_buurten_ligt_in_wijk_id_seq TO write_gebieden", + ] + + apply_schema_and_profile_permissions( + engine, "public", ams_schema, profiles, "level_b2", "LEVEL/B2", verbose=1 + ) + grants = _filter_grant_statements(caplog) + assert grants == [ + "GRANT SELECT (eind_geldigheid) ON TABLE public.gebieden_bouwblokken TO level_b2", + "GRANT SELECT (id) ON TABLE public.gebieden_bouwblokken TO level_b2", + "GRANT SELECT (ligt_in_buurt_id) ON TABLE public.gebieden_bouwblokken TO level_b2", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_bouwblokken TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_buurten TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_buurten_ligt_in_wijk TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_wijken TO write_gebieden", + "GRANT USAGE ON SEQUENCE public.gebieden_buurten_ligt_in_wijk_id_seq TO write_gebieden", + ] + + apply_schema_and_profile_permissions( + engine, "public", ams_schema, profiles, "level_c2", "LEVEL/C2", verbose=1 + ) + grants = _filter_grant_statements(caplog) + assert grants == [ + "GRANT SELECT (begin_geldigheid) ON TABLE public.gebieden_bouwblokken TO level_c2", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_bouwblokken TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_buurten TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_buurten_ligt_in_wijk TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_wijken TO write_gebieden", + "GRANT USAGE ON SEQUENCE public.gebieden_buurten_ligt_in_wijk_id_seq TO write_gebieden", + ] # Check if the read priviliges are correct _check_select_permission_denied(engine, "level_a1", "gebieden_bouwblokken") @@ -321,7 +529,7 @@ def test_auth_list_permissions(self, here, engine, gebieden_schema_auth_list, db _check_delete_permission_denied(engine, "level_b1", "gebieden_bouwblokken", "id = 'abc'") _check_truncate_permission_denied(engine, "level_b1", "gebieden_bouwblokken") - def test_auto_create_roles(self, here, engine, gebieden_schema_auth, dbsession): + def test_auto_create_roles(self, here, engine, gebieden_schema_auth, dbsession, caplog): """ Prove that dataset, table, and field permissions are set according, to the "OF-OF" Exclusief principle: @@ -356,9 +564,49 @@ def test_auto_create_roles(self, here, engine, gebieden_schema_auth, dbsession): # _check_role_does_not_exist(engine, "scope_level_c") # Apply the permissions from Schema and Profiles. - apply_schema_and_profile_permissions( - engine, "public", ams_schema, profiles, "AUTO", "ALL", create_roles=True - ) + with caplog.at_level(logging.INFO, logger="schematools.permissions.db"): + apply_schema_and_profile_permissions( + engine, "public", ams_schema, profiles, "AUTO", "ALL", create_roles=True, verbose=1 + ) + + grants = _filter_grant_statements(caplog) + assert grants == [ + "GRANT SELECT (begin_geldigheid) ON TABLE public.gebieden_bouwblokken TO scope_level_c", + "GRANT SELECT (begingeldigheid) ON TABLE public.gebieden_ggwgebieden TO scope_level_a", + "GRANT SELECT (eind_geldigheid) ON TABLE public.gebieden_bouwblokken TO scope_level_b", + "GRANT SELECT (eindgeldigheid) ON TABLE public.gebieden_ggwgebieden TO scope_level_a", + "GRANT SELECT (id) ON TABLE public.gebieden_bouwblokken TO scope_level_b", + "GRANT SELECT (id) ON TABLE public.gebieden_ggwgebieden TO scope_level_a", + "GRANT SELECT (identificatie) ON TABLE public.gebieden_ggwgebieden TO scope_level_a", + "GRANT SELECT (ligt_in_buurt_id) ON TABLE public.gebieden_bouwblokken TO scope_level_d", + "GRANT SELECT (ligt_in_buurt_identificatie) ON TABLE public.gebieden_bouwblokken TO scope_level_d", + "GRANT SELECT (ligt_in_buurt_loose_id) ON TABLE public.gebieden_bouwblokken TO scope_level_d", + "GRANT SELECT (ligt_in_buurt_volgnummer) ON TABLE public.gebieden_bouwblokken TO scope_level_d", + "GRANT SELECT (volgnummer) ON TABLE public.gebieden_ggwgebieden TO scope_level_a", + "GRANT SELECT ON SEQUENCE public.gebieden_bouwblokken_ligt_in_buurt_id_seq TO scope_level_d", + "GRANT SELECT ON SEQUENCE public.gebieden_buurten_ligt_in_wijk_id_seq TO scope_level_a", + "GRANT SELECT ON SEQUENCE public.gebieden_ggwgebieden_bestaat_uit_buurten_id_seq TO scope_level_e", + "GRANT SELECT ON SEQUENCE public.gebieden_ggwgebieden_gebieds_grenzen_id_seq TO scope_level_f", + "GRANT SELECT ON TABLE public.gebieden_bouwblokken_ligt_in_buurt TO scope_level_d", + "GRANT SELECT ON TABLE public.gebieden_buurten TO scope_level_a", + "GRANT SELECT ON TABLE public.gebieden_buurten_ligt_in_wijk TO scope_level_a", + "GRANT SELECT ON TABLE public.gebieden_ggwgebieden_bestaat_uit_buurten TO scope_level_e", + "GRANT SELECT ON TABLE public.gebieden_ggwgebieden_gebieds_grenzen TO scope_level_f", + "GRANT SELECT ON TABLE public.gebieden_wijken TO scope_level_a", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_bouwblokken TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_bouwblokken_ligt_in_buurt TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_buurten TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_buurten_ligt_in_wijk TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_ggwgebieden TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_ggwgebieden_bestaat_uit_buurten TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_ggwgebieden_gebieds_grenzen TO write_gebieden", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.gebieden_wijken TO write_gebieden", + "GRANT USAGE ON SEQUENCE public.gebieden_bouwblokken_ligt_in_buurt_id_seq TO write_gebieden", + "GRANT USAGE ON SEQUENCE public.gebieden_buurten_ligt_in_wijk_id_seq TO write_gebieden", + "GRANT USAGE ON SEQUENCE public.gebieden_ggwgebieden_bestaat_uit_buurten_id_seq TO write_gebieden", + "GRANT USAGE ON SEQUENCE public.gebieden_ggwgebieden_gebieds_grenzen_id_seq TO write_gebieden", + ] + # Check if roles exist and the read priviliges are correct _check_select_permission_denied(engine, "scope_level_a", "gebieden_bouwblokken") _check_select_permission_granted(engine, "scope_level_a", "gebieden_buurten") @@ -425,7 +673,6 @@ def test_single_dataset_permissions( importer.generate_db_objects("wijken", truncate=True, ind_extra_index=False) # dataset 2: meetbouten - ndjson_path = here / "files" / "data" / "meetbouten.ndjson" importer = NDJSONImporter(meetbouten_schema, engine) importer.generate_db_objects("meetbouten", truncate=True, ind_extra_index=False) importer.generate_db_objects("metingen", truncate=True, ind_extra_index=False) @@ -465,7 +712,7 @@ def test_single_dataset_permissions( # Check perms again on meetbouten _check_select_permission_granted(engine, "scope_openbaar", "meetbouten_meetbouten") - def test_permissions_support_shortnames(self, here, engine, hr_schema_auth, dbsession): + def test_permissions_support_shortnames(self, here, engine, hr_schema_auth, dbsession, caplog): """ Prove that table, and field permissions are set on the shortnamed field. """ @@ -479,12 +726,36 @@ def test_permissions_support_shortnames(self, here, engine, hr_schema_auth, dbse ams_schema = {hr_schema_auth.id: hr_schema_auth} # Apply the permissions from Schema and Profiles. - apply_schema_and_profile_permissions( - engine, "public", ams_schema, None, "level_b", "LEVEL/B", create_roles=True - ) - apply_schema_and_profile_permissions( - engine, "public", ams_schema, None, "level_c", "LEVEL/C", create_roles=True - ) + with caplog.at_level(logging.INFO, logger="schematools.permissions.db"): + apply_schema_and_profile_permissions( + engine, + "public", + ams_schema, + None, + "level_b", + "LEVEL/B", + create_roles=True, + verbose=1, + ) + apply_schema_and_profile_permissions( + engine, + "public", + ams_schema, + None, + "level_c", + "LEVEL/C", + create_roles=True, + verbose=1, + ) + + grants = _filter_grant_statements(caplog) + assert grants == [ + "GRANT SELECT (identifier) ON TABLE public.hr_sbi_ac TO level_b", + "GRANT SELECT (sbi_ac_naam) ON TABLE public.hr_sbi_ac TO level_b", + "GRANT SELECT (sbi_ac_no) ON TABLE public.hr_sbi_ac TO level_c", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.hr_sbi_ac TO write_hr", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.hr_sbi_ac TO write_hr", + ] # Check if the read priviliges are correct _check_select_permission_granted(engine, "level_b", "hr_sbi_ac", "sbi_ac_naam") @@ -652,7 +923,7 @@ def test_permissions_support_shortnames(self, here, engine, hr_schema_auth, dbse "'berry','14641','15101051'", ) - def test_setting_additional_grants(self, here, engine, meetbouten_schema, dbsession): + def test_setting_additional_grants(self, here, engine, meetbouten_schema, dbsession, caplog): """ Prove that additional grants can be set using the extra argument. """ @@ -667,16 +938,38 @@ def test_setting_additional_grants(self, here, engine, meetbouten_schema, dbsess connection.execute("CREATE TABLE datasets_dataset (id integer)") # Apply the permissions to meetbouten and add the extra grants to datasets_dataset - apply_schema_and_profile_permissions( - engine, - "public", - meetbouten_schema, - None, - "AUTO", - "ALL", - create_roles=True, - additional_grants=("datasets_dataset:SELECT;scope_openbaar",), - ) + with caplog.at_level(logging.INFO, logger="schematools.permissions.db"): + apply_schema_and_profile_permissions( + engine, + "public", + meetbouten_schema, + None, + "AUTO", + "ALL", + create_roles=True, + verbose=1, + additional_grants=("datasets_dataset:SELECT;scope_openbaar",), + ) + + grants = _filter_grant_statements(caplog) + assert grants == [ + "GRANT SELECT ON SEQUENCE public.meetbouten_meetbouten_ligt_in_buurt_id_seq TO scope_openbaar", + "GRANT SELECT ON SEQUENCE public.meetbouten_metingen_refereertaanreferentiepunten_id_seq TO scope_openbaar", + "GRANT SELECT ON TABLE public.datasets_dataset TO scope_openbaar", + "GRANT SELECT ON TABLE public.meetbouten_meetbouten TO scope_openbaar", + "GRANT SELECT ON TABLE public.meetbouten_meetbouten_ligt_in_buurt TO scope_openbaar", + "GRANT SELECT ON TABLE public.meetbouten_metingen TO scope_openbaar", + "GRANT SELECT ON TABLE public.meetbouten_metingen_refereertaanreferentiepunten TO scope_openbaar", + "GRANT SELECT ON TABLE public.meetbouten_referentiepunten TO scope_openbaar", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.meetbouten_meetbouten TO write_meetbouten", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.meetbouten_meetbouten_ligt_in_buurt TO write_meetbouten", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.meetbouten_metingen TO write_meetbouten", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.meetbouten_metingen_refereertaanreferentiepunten TO write_meetbouten", + "GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON TABLE public.meetbouten_referentiepunten TO write_meetbouten", + "GRANT USAGE ON SEQUENCE public.meetbouten_meetbouten_ligt_in_buurt_id_seq TO write_meetbouten", + "GRANT USAGE ON SEQUENCE public.meetbouten_metingen_refereertaanreferentiepunten_id_seq TO write_meetbouten", + ] + # Check perms on the datasets_dataset table _check_select_permission_granted(engine, "scope_openbaar", "datasets_dataset") @@ -704,11 +997,12 @@ def _check_select_permission_denied(engine, role, table, column="*"): """Check if role has no SELECT permission on table. Fail if role, table or column does not exist. """ - with pytest.raises(Exception) as e_info, engine.begin() as connection: + with pytest.raises( + Exception, match=f"permission denied for table {table}" + ), engine.begin() as connection: connection.execute(f"SET ROLE {role}") connection.execute(f"SELECT {column} FROM {table}") connection.execute("RESET ROLE") - assert f"permission denied for table {table}" in str(e_info) def _check_select_permission_granted(engine, role, table, column="*"): @@ -806,3 +1100,21 @@ def _check_truncate_permission_denied(engine, role, table): connection.execute(f"TRUNCATE {table}") connection.execute("RESET ROLE") assert f"permission denied for table {table}" in str(e_info) + + +def _filter_grant_statements(caplog): + grants = sorted( + m.replace("Executed --> ", "") + for m in caplog.messages + # Be specific in what is excluded, so unexpected notices can be detected. + if not m.endswith('" already exists, skipping') and ("CREATE ROLE" not in m) + ) + + # Writes are seen multple times, because they use a single role. + seen = set() + seen_twice = {m for m in grants if (" TO write_" not in m) and (m in seen or seen.add(m))} + newline = "\n" # Python 3.10 f-string syntax doesn't support \ + assert not seen_twice, f"Duplicate grants: {newline.join(seen_twice)}" + + caplog.clear() + return grants