ServicesÀ proposNotesContact Me contacter →
EN FR
Note

Matérialisation de table sécurisée dans dbt

Une matérialisation dbt personnalisée qui réapplique automatiquement les politiques d'accès aux lignes BigQuery, les descriptions de colonnes et les tags de masquage des données après chaque reconstruction de table.

Planté
dbtbigquerydata engineeringdata quality

BigQuery prend en charge des politiques d’accès aux lignes qui restreignent les lignes visibles par différents groupes d’utilisateurs. Quand dbt supprime et recrée une table — comme le fait la matérialisation standard table — toute politique d’accès aux lignes sur cette table disparaît. Vous devez les réappliquer après chaque build.

Le comportement par défaut de dbt (supprimer + recréer) entre en conflit avec le modèle de sécurité de BigQuery : les politiques sont attachées à des objets de table spécifiques et sont perdues à chaque reconstruction. Chaque fois que dbt remplace la table, la configuration de sécurité revient à zéro.

Pourquoi les post-hooks ne passent pas à l’échelle

Pour un modèle unique avec une seule politique, les post-hooks fonctionnent bien :

{{ config(
materialized='table',
post_hook="CREATE OR REPLACE ROW ACCESS POLICY region_filter ON {{ this }} GRANT TO (\"group:eu-analysts@company.com\") FILTER USING (region = 'EU')"
) }}

Cela cesse de passer à l’échelle autour de trois politiques par modèle. Le bloc de config devient illisible, les guillemets deviennent fragiles (guillemets doubles imbriqués dans des chaînes SQL dans Jinja), et appliquer le même pattern sur plusieurs modèles signifie copier des murs de texte post-hook. Si une exigence de sécurité change — par exemple, vous ajoutez un nouveau groupe d’analystes — vous devez chercher dans une trentaine de modèles pour mettre à jour les post-hooks.

Une matérialisation personnalisée gère cela de manière déclarative : les politiques sont définies dans la config et appliquées automatiquement après chaque build de table. Ajouter une politique se réduit à une entrée de dictionnaire dans la config du modèle plutôt qu’une chaîne SQL manuellement échappée.

Le pattern

Créez macros/materializations/secured_table.sql :

{%- materialization secured_table, adapter='bigquery' -%}
{# Setup relations #}
{% set target_relation = this.incorporate(type='table') %}
{% set existing_relation = load_cached_relation(this) %}
{{ run_hooks(pre_hooks) }}
{# Drop and rebuild #}
{% if existing_relation is not none %}
{% do adapter.drop_relation(existing_relation) %}
{% endif %}
{% call statement('main') %}
{{ create_table_as(false, target_relation, sql) }}
{% endcall %}
{# Apply column descriptions from config #}
{% set column_descriptions = config.get('column_descriptions', {}) %}
{% for col_name, description in column_descriptions.items() %}
{% call statement('describe_' ~ col_name) %}
ALTER TABLE {{ target_relation }}
ALTER COLUMN {{ col_name }}
SET OPTIONS(description='{{ description | replace("'", "\\'") }}')
{% endcall %}
{% endfor %}
{# Apply row access policies from config #}
{% set row_access_policies = config.get('row_access_policies', []) %}
{% for policy in row_access_policies %}
{% call statement('policy_' ~ policy.name) %}
CREATE OR REPLACE ROW ACCESS POLICY {{ policy.name }}
ON {{ target_relation }}
GRANT TO ({{ policy.grantees | join(', ') }})
FILTER USING ({{ policy.filter_expression }})
{% endcall %}
{% endfor %}
{{ run_hooks(post_hooks) }}
{% set grant_config = config.get('grants') %}
{% do apply_grants(target_relation, grant_config) %}
{% do persist_docs(target_relation, model) %}
{% do adapter.commit() %}
{{ return({'relations': [target_relation]}) }}
{%- endmaterialization -%}

Utilisation dans un modèle

Les politiques de sécurité sont définies dans le bloc de config comme données :

{{ config(
materialized='secured_table',
column_descriptions={
'user_id': 'Identifiant unique provenant du système d\'authentification',
'email': 'Adresse e-mail de l\'utilisateur, restreinte aux groupes autorisés PII',
'region': 'Code de région géographique utilisé pour le filtrage d\'accès',
'lifetime_value': 'Revenu total attribué à cet utilisateur en EUR'
},
row_access_policies=[
{
'name': 'eu_analysts_filter',
'grantees': ['"group:eu-analysts@company.com"'],
'filter_expression': "region = 'EU'"
},
{
'name': 'us_analysts_filter',
'grantees': ['"group:us-analysts@company.com"'],
'filter_expression': "region = 'US'"
},
{
'name': 'admin_full_access',
'grantees': ['"group:data-admins@company.com"'],
'filter_expression': 'TRUE'
}
]
) }}
SELECT
user_id,
email,
region,
lifetime_value
FROM {{ ref('int__users_enriched') }}

Chaque fois que ce modèle est exécuté, il se reconstruit avec les descriptions de colonnes et les politiques d’accès aux lignes appliquées automatiquement.

Descriptions de colonnes : deux approches valides

Les descriptions de colonnes dans cette matérialisation ont un chemin alternatif : persist_docs de dbt avec columns: true lit les descriptions depuis les fichiers YAML de schéma et les applique via l’adaptateur. Cela fonctionne bien si votre équipe maintient déjà des YAML de schéma.

L’approche par matérialisation garde les descriptions dans la config du modèle aux côtés du SQL, ce que certaines équipes préfèrent — tout ce qui concerne la définition de la table vit dans un seul fichier. Les deux approches sont valides. L’approche par matérialisation est avantageuse quand :

  • Les descriptions de colonnes sont étroitement liées aux préoccupations de sécurité (documenter ce qui est restreint et pourquoi)
  • Vous souhaitez que la config de sécurité et la documentation soient au même endroit pour l’auditabilité
  • Votre équipe ne maintient pas de fichiers YAML de schéma complets

Si vous avez déjà des fichiers schema.yml exhaustifs avec des descriptions de colonnes, ne les dupliquez pas dans la config de matérialisation. Utilisez persist_docs pour les descriptions et la matérialisation uniquement pour les politiques de sécurité.

Extension avec le masquage des données

La sécurité au niveau des colonnes dans BigQuery utilise des policy tags provenant de Data Catalog. Le pattern est similaire aux politiques d’accès aux lignes, mais le DDL assigne des policy tags aux colonnes plutôt que de créer des politiques autonomes :

{# Add this block after row access policies in the materialization #}
{% set column_policy_tags = config.get('column_policy_tags', {}) %}
{% for col_name, tag_path in column_policy_tags.items() %}
{% call statement('tag_' ~ col_name) %}
ALTER TABLE {{ target_relation }}
ALTER COLUMN {{ col_name }}
SET OPTIONS(policy_tags=['{{ tag_path }}'])
{% endcall %}
{% endfor %}

Puis dans la config du modèle :

{{ config(
materialized='secured_table',
column_policy_tags={
'email': 'projects/my-project/locations/us/taxonomies/123/policyTags/456',
'phone': 'projects/my-project/locations/us/taxonomies/123/policyTags/789'
}
) }}

La taxonomie et les policy tags doivent exister dans Data Catalog avant que la matérialisation ne les référence — créez-les dans le cadre de votre configuration d’infrastructure (Terraform, gcloud CLI, etc.). La matérialisation ne gère que l’assignation au niveau colonne à chaque reconstruction.

Une seule matérialisation peut couvrir les politiques d’accès aux lignes, les descriptions de colonnes et les tags de masquage des données. La couche IAM contrôle l’accès aux datasets et aux requêtes ; cette matérialisation contrôle ce qui est visible à l’intérieur d’une table.

Combinaison avec le zéro-downtime

Le pattern de table sécurisée présenté ici utilise le simple drop-and-recreate. Pour les tables de production qui nécessitent à la fois des garanties de sécurité et de disponibilité, vous pouvez le combiner avec le pattern de swap zero-downtime. Construisez vers un nom temporaire, validez, swappez, puis appliquez les politiques de sécurité à la table finale. La différence clé : les politiques d’accès aux lignes doivent être appliquées après le swap, à la table qui occupe désormais le nom cible, pas à la table temporaire qui a été renommée.

Ce pattern combiné est plus complexe à maintenir, donc utilisez-le uniquement si vous avez vraiment besoin des deux capacités. La plupart des tables sécurisées sont des modèles analytiques pour lesquels une brève interruption lors de la reconstruction est acceptable.