From 21a3be37772c790d686aa22cd1303e20091c334f Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Wed, 25 Sep 2024 22:19:10 +0200 Subject: [PATCH] Refactor and fix get_all_dataset_scopes() This allows passing table-type so sequence permissions can also be assigned later. Fix passing auth-permissions to through/nested tables. The nested table permissions were redefined by looping over the full tables, which didn't receive the field.auth data but exposed dataset auth instead. Also there is no need to define auth on their fields if the full table has auth set. --- src/schematools/permissions/db.py | 174 +++++++++++++----------------- src/schematools/types.py | 5 +- tests/test_permissions.py | 6 +- 3 files changed, 78 insertions(+), 107 deletions(-) diff --git a/src/schematools/permissions/db.py b/src/schematools/permissions/db.py index fa35ee8f..11f053c4 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 @@ -173,15 +173,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, @@ -193,11 +194,20 @@ def set_dataset_write_permissions( ) -def get_all_dataset_scopes( - ams_schema: DatasetSchema, - role: str, - scope: str, -) -> defaultdict[str, list]: +@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) -> list[GrantParam]: """Returns all scopes that should be applied to the tables of a dataset. Args: @@ -222,39 +232,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 +257,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 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.is_subfield: + field_scopes = (field.parent_field.auth - {PUBLIC_SCOPE}) or field_scopes - 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,22 +283,24 @@ 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) - ), - } + all_scopes.append( + GrantParam( + # NB. space after SELECT is significant! + privileges=[f"SELECT ({column_name})"], + target_type=PgObjectType.TABLE, + target=table_name, + grantees=_fetch_grantees(column_scopes.get(column_name, table_scopes)), + ) ) else: if table_name not in all_scopes: - all_scopes[table_name].append( - { - "privileges": ["SELECT"], - "grantees": _fetch_grantees(fallback_scope), - } + all_scopes.append( + GrantParam( + privileges=["SELECT"], + target_type=PgObjectType.TABLE, + target=table_name, + grantees=_fetch_grantees(table_scopes), + ) ) return all_scopes @@ -366,32 +343,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(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( 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/test_permissions.py b/tests/test_permissions.py index c5146631..a3579340 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -545,21 +545,17 @@ def test_auto_create_roles(self, here, engine, gebieden_schema_auth, dbsession, 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 (diemen) ON TABLE public.gebieden_ggwgebieden_gebieds_grenzen 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 (id) ON TABLE public.gebieden_ggwgebieden_gebieds_grenzen 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 (parent_id) ON TABLE public.gebieden_ggwgebieden_gebieds_grenzen TO scope_level_f", "GRANT SELECT (volgnummer) ON TABLE public.gebieden_ggwgebieden TO scope_level_a", - "GRANT SELECT (zaanstad) ON TABLE public.gebieden_ggwgebieden_gebieds_grenzen TO scope_level_a", - "GRANT SELECT ON TABLE public.gebieden_bouwblokken_ligt_in_buurt TO scope_level_a", + "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",