I tried checking how the 6 types of data protection policies on the provider side work on the consumer side in Snowflake Data Sharing

I tried checking how the 6 types of data protection policies on the provider side work on the consumer side in Snowflake Data Sharing

We conducted empirical verification of the behavior of six data protection policy types in Snowflake Direct Share, examining both the provider and consumer sides. This document provides a detailed summary of how each policy, including masking, row access, and projection policies, functions across shares, as well as how context functions within policies are evaluated.
2026.09.01

This page has been translated by machine translation. View original

This is Kawabata.

In this article, I will comprehensively verify how the 6 types of data protection policies defined on the provider side (masking, row access, projection, aggregation, join, and Data Movement Policy) behave on the consumer side.

https://docs.snowflake.com/en/user-guide/data-sharing-policy-protected-data

Conclusion

These are the results measured by connecting two Enterprise Edition accounts in the same organization in the AWS Tokyo region via direct share, as of August 31, 2026.

Policy Does it take effect on the consumer side? Notes
Masking Policy Works even when tag-based policy body is in a non-shared DB
Row Access Policy Evaluated with owner privileges even when mapping table is in a non-shared DB
Projection Policy Compile-time error. Can differentiate per share with INVOKER_SHARE()
Aggregation Policy Groups below the minimum are aggregated into a remainder group
Join Policy Requirements can be met by joining with a consumer-owned table
Data Movement Policy Tag-based works / Account-level does not Restricts fetch and unload. However, CTAS passes through

There were two important points.

  1. A policy written to "mask only for a specific role" will expose raw values to all consumers across a share (because CURRENT_ROLE() becomes NULL inside the policy, failing to match the mask condition)
-- Fail-closed approach (safe): only shows raw value for permitted roles
CASE WHEN CURRENT_ROLE() IN ('ANALYST_ROLE') THEN val
     ELSE '***MASKED***' END    -- ← masked even when the role cannot be determined

-- Fail-open approach (dangerous): masks only for a specific role
CASE WHEN CURRENT_ROLE() IN ('UNTRUSTED_ROLE') THEN '***MASKED***'
     ELSE val END               -- ← raw value is exposed when role cannot be determined
  1. Policies control query results and certain data movements, but cannot stop CTAS or re-sharing of data that has already been retrieved

Prerequisites

  • Both accounts must be Enterprise Edition or higher
  • Accounts must be in the same region
  • Verification date: August 31, 2026, Snowflake Enterprise Edition, AWS Tokyo region
  • Account identifiers in this article have been replaced with sample values (myorg.provider_acct / myorg.consumer_acct)

Verification Perspectives

For each policy type, the following perspectives were measured.

Perspective What was checked
Application Is it enforced by the consumer's query? Is the error at compile time or runtime?
Context functions How are CURRENT_ROLE() etc. inside the policy body evaluated on the consumer side?
Definition visibility Can the consumer see the policy body or tags?
Consumer-side application Can the consumer add policies to shared objects?
Change propagation When are provider's policy changes or removals reflected?
Protection after export Does protection remain after CTAS, re-sharing, or unloading?

Preparation

We create a verification DB and two shares on the provider side. The reason for creating two shares is to demonstrate per-share branching via INVOKER_SHARE() (described later) with a single consumer.

-- Provider side
CREATE DATABASE POLSHARE_DB;       -- Target for sharing
CREATE DATABASE POLSHARE_LIB_DB;   -- Not shared (centralized policy management)

CREATE SHARE POLSHARE_SHARE_A;
GRANT USAGE ON DATABASE POLSHARE_DB TO SHARE POLSHARE_SHARE_A;
-- (Granting schema USAGE and table SELECT is omitted)
ALTER SHARE POLSHARE_SHARE_A ADD ACCOUNTS = myorg.consumer_acct;

CREATE SHARE POLSHARE_SHARE_B;   -- Also share the same table via a different share
GRANT USAGE ON DATABASE POLSHARE_DB TO SHARE POLSHARE_SHARE_B;
-- (Grant schema USAGE and table SELECT for target schema as with SHARE_A)
ALTER SHARE POLSHARE_SHARE_B ADD ACCOUNTS = myorg.consumer_acct;

Mount the shares on the consumer side.

-- Consumer side
CREATE DATABASE POLSHARE_A_DB FROM SHARE myorg.provider_acct.POLSHARE_SHARE_A;
CREATE DATABASE POLSHARE_B_DB FROM SHARE myorg.provider_acct.POLSHARE_SHARE_B;
GRANT IMPORTED PRIVILEGES ON DATABASE POLSHARE_A_DB TO ROLE POLSHARE_CONSUMER_ADMIN;
GRANT IMPORTED PRIVILEGES ON DATABASE POLSHARE_B_DB TO ROLE POLSHARE_CONSUMER_ADMIN;

Note: For consumer-side verification, ALTER SESSION SET USE_CACHED_RESULT = FALSE; and USE SECONDARY ROLES NONE; are executed at the start of each session. The former is to prevent false positives in change propagation verification, and the latter is to block privilege bypasses via held roles.

Let's Try It

Masking Policy

To see the difference depending on how the policy is written, I applied policies with different approaches to each column in a single table.

Masking Policy
-- Allowlist type: shows raw value only for permitted roles
CREATE MASKING POLICY MP_ALLOWLIST AS (val STRING) RETURNS STRING ->
  CASE WHEN CURRENT_ROLE() IN ('POLSHARE_PROV_ADMIN') THEN val
       ELSE '***MASKED***' END;

-- Blocklist type: masks only for a specific role (raw value is shown when role cannot be determined = fail-open)
CREATE MASKING POLICY MP_BLOCKLIST AS (val STRING) RETURNS STRING ->
  CASE WHEN CURRENT_ROLE() IN ('UNTRUSTED_ROLE') THEN '***MASKED***'
       ELSE val END;

-- Debug policy that returns the evaluated values of context functions as-is
CREATE MASKING POLICY MP_DEBUG AS (val STRING) RETURNS STRING ->
     'ROLE='      || NVL(CURRENT_ROLE(), '<NULL>')
  || '|USER='     || NVL(CURRENT_USER(), '<NULL>')
  || '|ACCTNAME=' || NVL(CURRENT_ACCOUNT_NAME(), '<NULL>')
  || '|SHARE='    || NVL(INVOKER_SHARE(), '<NULL>');

Results of SELECT on the consumer side.

Column (approach) Provider Consumer
Always masked ***ALWAYS*** ***ALWAYS***
Always masked (policy body in non-shared DB) ***ALWAYS_LIB*** ***ALWAYS_LIB***
Allowlist type (MP_ALLOWLIST) Raw value ***MASKED***
Blocklist type (MP_BLOCKLIST) Raw value Raw value (leaks)
Debug column (MP_DEBUG) ROLE=POLSHARE_PROV_ADMIN|... ROLE=<NULL>|USER=<NULL>|ACCTNAME=CONSUMER_ACCT|SHARE=POLSHARE_SHARE_A

2026-09-01_15h49_55

As the debug column shows, inside a policy on a shared object, CURRENT_ROLE() / CURRENT_USER() become NULL. This causes the CASE condition to be UNKNOWN, falling through to the ELSE clause.

2026-09-01_15h49_42

  • Allowlist type: ELSE is masked → fails closed (safe side)
  • Blocklist type: ELSE is raw value → raw value leaks to all consumers

The blocklist approach works within the same account, but when placed on a share, the role cannot be determined, causing it to fall to the side that exposes the raw value (fail-open). To flip to the safe side without drastically changing the existing policy design, you can explicitly detect NULL.

CREATE MASKING POLICY MP_NULLSAFE AS (val STRING) RETURNS STRING ->
  CASE WHEN CURRENT_ROLE() IS NULL THEN '***SHARE_CTX***'   -- Cross-share access
       WHEN CURRENT_ROLE() IN ('POLSHARE_PROV_ADMIN') THEN val
       ELSE '***MASKED***' END;

On the other hand, CURRENT_ACCOUNT() / CURRENT_ACCOUNT_NAME() / CURRENT_ORGANIZATION_NAME() / INVOKER_SHARE() are correctly evaluated with the consumer-side values. Use these for branching in share-aware policies.

-- Raw value for provider only, masked for shared destinations
CREATE MASKING POLICY MP_ACCT AS (val STRING) RETURNS STRING ->
  CASE WHEN CURRENT_ACCOUNT_NAME() = 'PROVIDER_ACCT' THEN val
       ELSE '***CONSUMER***' END;

Note: Since CURRENT_ACCOUNT() (the locator) can change during account migration, using the combination of CURRENT_ACCOUNT_NAME() and CURRENT_ORGANIZATION_NAME() is recommended.

Tag-based masking was also enforced in the same way. Additionally, placing the policy body in a separate DB that is not shared had no impact on enforcement across the share.

Row Access Policy

Since a row access policy only returns rows that evaluate to TRUE, rows where the condition evaluates to NULL (UNKNOWN) are not returned. Unlike masking policies, it fails closed (safe side).

Row Access Policy
-- Role-dependent RAP: CURRENT_ROLE()=NULL across a share → all rows excluded (0 rows)
CREATE OR REPLACE ROW ACCESS POLICY POLSHARE_DB.S_RAP.RAP_ROLE
  AS (region STRING) RETURNS BOOLEAN ->
  CURRENT_ROLE() IN ('POLSHARE_PROV_ADMIN', 'ACCOUNTADMIN');

-- Account-branching RAP: only shows APAC to consumers (multi-tenant delivery pattern)
CREATE OR REPLACE ROW ACCESS POLICY POLSHARE_DB.S_RAP.RAP_ACCT
  AS (region STRING) RETURNS BOOLEAN ->
  CURRENT_ACCOUNT_NAME() = 'PROVIDER_ACCT' OR region = 'APAC';

-- Mapping table reference RAP (mapping included in the share)
CREATE OR REPLACE ROW ACCESS POLICY POLSHARE_DB.S_RAP.RAP_MAP_S
  AS (region STRING) RETURNS BOOLEAN ->
  EXISTS (SELECT 1 FROM POLSHARE_DB.S_RAP.REGION_MAP m
          WHERE m.region = region AND m.account_name = CURRENT_ACCOUNT_NAME());

-- Mapping table reference RAP (mapping in non-shared LIB DB. Works because evaluated with owner privileges)
CREATE OR REPLACE ROW ACCESS POLICY POLSHARE_DB.S_RAP.RAP_MAP_L
  AS (region STRING) RETURNS BOOLEAN ->
  EXISTS (SELECT 1 FROM POLSHARE_LIB_DB.MAPS.REGION_MAP_LIB m
          WHERE m.region = region AND m.account_name = CURRENT_ACCOUNT_NAME());

Policy condition Provider Consumer
CURRENT_ROLE() IN (...) 6 rows 0 rows (NULL → all rows excluded)
CURRENT_ACCOUNT_NAME() = '...' OR region = 'APAC' 6 rows 2 rows (APAC only)
Mapping table reference (table in share) 6 rows 2 rows
Mapping table reference (table in non-shared DB) 6 rows 2 rows

2026-09-01_15h58_16

Even if the mapping table is not included in the share, the policy is evaluated with owner privileges so no error occurs. This allows row-level differentiation per tenant while keeping the mapping hidden from consumers.

Projection Policy

I measured the INVOKER_SHARE() branching shown in the official documentation sample.

Projection Policy
-- Per-share branching (official sample form): projection disallowed only via SHARE_A,
-- allowed via SHARE_B and by the provider itself
CREATE OR REPLACE PROJECTION POLICY POLSHARE_DB.S_PROJ.PP_BY_SHARE
  AS () RETURNS PROJECTION_CONSTRAINT ->
  CASE WHEN INVOKER_SHARE() = 'POLSHARE_SHARE_A' THEN PROJECTION_CONSTRAINT(ALLOW => false)
       ELSE PROJECTION_CONSTRAINT(ALLOW => true) END;

Even with the same table and the same consumer, results differed depending on which share was used.

SELECT email FROM POLSHARE_A_DB.S_PROJ.CONTACTS;  -- Via SHARE_A

2026-09-01_15h59_47

SELECT email FROM POLSHARE_B_DB.S_PROJ.CONTACTS;  -- Via SHARE_B → raw value retrieved

2026-09-01_16h00_30
The same error occurs with EXPLAIN, so enforcement is at compile time. Note that non-projecting references such as WHERE email = '...' are allowed, so value existence inference is possible.

Aggregation Policy

Aggregation Policy
-- Provider has no restrictions; all others have a minimum group size of 5 (smaller groups go to remainder)
CREATE OR REPLACE AGGREGATION POLICY POLSHARE_DB.S_AGG.AP_COND
  AS () RETURNS AGGREGATION_CONSTRAINT ->
  CASE WHEN CURRENT_ACCOUNT_NAME() = 'PROVIDER_ACCT' THEN NO_AGGREGATION_CONSTRAINT()
       ELSE AGGREGATION_CONSTRAINT(MIN_GROUP_SIZE => 5) END;

A plain SELECT * returns an error.

2026-09-01_16h02_00

Results when a consumer aggregates a table with MIN_GROUP_SIZE => 5 set (APAC: 12 rows / EMEA: 5 rows / US: 3 rows).

SELECT region, COUNT(*) AS cnt FROM POLSHARE_A_DB.S_AGG.SALES GROUP BY region;

2026-09-01_16h05_33

Join Policy

Join Policy
-- Consumer side: succeeds when joined with a table in their own DB
SELECT p.diagnosis, COUNT(*) AS cnt
FROM POLSHARE_A_DB.S_JOIN.PATIENTS p
JOIN POLSHARE_LOCAL_DB.WORK.LOCAL_PATIENTS l ON p.patient_id = l.patient_id
GROUP BY p.diagnosis;

A table with JOIN_CONSTRAINT(JOIN_REQUIRED => TRUE) set will block SELECT * with an error. When the consumer joins it with their own table, retrieval succeeds.

2026-09-01_16h06_47

2026-09-01_16h09_45

WHERE ... IN (subquery) does not satisfy the join requirement and results in the same error.

2026-09-01_16h10_10

Data Movement Policy

Data Movement Policy (DMP) is a data export control feature that became GA in August 2026, and tag-based application is also included in the GA release.
The article from a previous verification is below.

https://dev.classmethod.jp/articles/snowflake-data-movement-policies-ga/

The official documentation only contains the sentence "Cross-region share protection is not supported," and the behavior with same-region shares is not described. Based on actual measurements, tag-based DMP worked across shares.

Data Movement Policy
-- Limit CLI/driver-based fetches to 100 rows (no limit for the verification role itself)
CREATE OR REPLACE DATA MOVEMENT RULE POLSHARE_DB.S_DMP.R_PSH_FETCH_100
  TYPE = 'PROGRAMMATIC_FETCH'
  MAX_ROWS AS () RETURNS INTEGER
  -> (CASE WHEN SYS_CONTEXT('SNOWFLAKE$SESSION', 'ROLE') = 'POLSHARE_PROV_ADMIN' THEN NULL ELSE 100 END)
  COMMENT = 'Fetch limited to 100 rows except provider admin';

-- Also limit Snowsight result display to 100 rows (additional rule for taking screenshots in Snowsight.
-- The actual error 100168 in the article body is from the PROGRAMMATIC_FETCH side)
CREATE OR REPLACE DATA MOVEMENT RULE POLSHARE_DB.S_DMP.R_PSH_SNOWSIGHT_100
  TYPE = 'SNOWSIGHT_UI'
  MAX_ROWS AS () RETURNS INTEGER
  -> (CASE WHEN SYS_CONTEXT('SNOWFLAKE$SESSION', 'ROLE') = 'POLSHARE_PROV_ADMIN' THEN NULL ELSE 100 END)
  COMMENT = 'Screenshot only: Snowsight display limited to 100 rows';

-- Block all unloads to internal/external stages (for COPY INTO verification)
CREATE OR REPLACE DATA MOVEMENT RULE POLSHARE_DB.S_DMP.R_PSH_COPY_INT_BLOCK
  TYPE = 'COPY_INTO_INTERNAL_STAGE'
  MAX_ROWS AS () RETURNS INTEGER
  -> (CASE WHEN SYS_CONTEXT('SNOWFLAKE$SESSION', 'ROLE') = 'POLSHARE_PROV_ADMIN' THEN NULL ELSE 0 END)
  COMMENT = 'Block unload to internal stage except provider admin';
CREATE OR REPLACE DATA MOVEMENT RULE POLSHARE_DB.S_DMP.R_PSH_COPY_EXT_BLOCK
  TYPE = 'COPY_INTO_EXTERNAL_STAGE'
  MAX_ROWS AS () RETURNS INTEGER
  -> (CASE WHEN SYS_CONTEXT('SNOWFLAKE$SESSION', 'ROLE') = 'POLSHARE_PROV_ADMIN' THEN NULL ELSE 0 END)
  COMMENT = 'Block unload to external stage except provider admin';

-- Policy that bundles rules as ENFORCE (blocks on violation)
CREATE OR REPLACE DATA MOVEMENT POLICY POLSHARE_DB.S_DMP.DMP_SHARE_GUARD
  ENFORCE_RULES = (R_PSH_FETCH_100, R_PSH_SNOWSIGHT_100, R_PSH_COPY_INT_BLOCK, R_PSH_COPY_EXT_BLOCK)
  COMMENT = 'Share guard: fetch/display <= 100 rows, block unload';

-- DMP is applied via tags (cannot be set on tags with PROPAGATE = NONE)
CREATE OR REPLACE TAG POLSHARE_DB.S_DMP.DMP_TAG
  PROPAGATE = ON_DEPENDENCY_AND_DATA_MOVEMENT;
ALTER TAG POLSHARE_DB.S_DMP.DMP_TAG SET DATA MOVEMENT POLICY POLSHARE_DB.S_DMP.DMP_SHARE_GUARD;
ALTER TABLE POLSHARE_DB.S_DMP.PAYROLL SET TAG POLSHARE_DB.S_DMP.DMP_TAG = 'guarded';

-- Policy for account-level verification (defined here only. Applied/removed with ALTER ACCOUNT SET/UNSET for testing)
CREATE OR REPLACE DATA MOVEMENT RULE POLSHARE_DB.S_DMP.R_PSH_BASELINE_FETCH_BLOCK
  TYPE = 'PROGRAMMATIC_FETCH'
  MAX_ROWS AS () RETURNS INTEGER
  -> (CASE WHEN SYS_CONTEXT('SNOWFLAKE$SESSION', 'ROLE') IN ('POLSHARE_PROV_ADMIN', 'ACCOUNTADMIN') THEN NULL ELSE 0 END)
  COMMENT = 'Baseline test: block fetch except provider roles';
CREATE OR REPLACE DATA MOVEMENT POLICY POLSHARE_DB.S_DMP.DMP_TEST_BASELINE
  ENFORCE_RULES = (R_PSH_BASELINE_FETCH_BLOCK)
  COMMENT = 'Temporary account baseline for share verification';

Results of operations from the consumer side, with a fetch limit of 100 rows and unload prohibition applied via tags on the provider side.

Consumer operation Result
SELECT COUNT(*) (1 row result) Success
SELECT * ... LIMIT 50 Success
SELECT * (fetch 500 rows) Runtime error
COPY INTO @own stage Compile-time error
CREATE TABLE ... AS SELECT * (500 rows) Success (passes through)

2026-09-01_16h14_17

2026-09-01_16h14_54

Error because it exceeds 100 rows

2026-09-01_16h15_50

Error because it is an unload to own stage
2026-09-01_16h16_23

CTAS passes through and succeeds because it is outside the movement type

2026-09-01_16h17_17

The important one is the last row.
Since CTAS is not included in DMP's control targets (movement types), all rows retrievable by a query can be copied to the consumer's own table. What is copied is purely the query result — it does not revert masked or row-access-policy-filtered data back to raw values. However, since tags and DMP are not propagated to the copy destination (PROPAGATE settings do not cross account boundaries), fetching and unloading from the copy destination are not restricted.

On the other hand, DMP applied at the account level (baseline) without tags did not block consumer operations. Only tag-based DMP works across shares.

Via Secure View

I also confirmed the configuration commonly used in practice: "don't share the base table, only share a secure view that references it."

Via Secure View
CREATE OR REPLACE TABLE POLSHARE_LIB_DB.BASE.CUSTOMERS_BASE (
  id NUMBER, region STRING, ssn STRING, email STRING
);
INSERT INTO POLSHARE_LIB_DB.BASE.CUSTOMERS_BASE VALUES
  (1, 'APAC', 'SSN-0001', 'base1@example.com'),
  (2, 'EMEA', 'SSN-0002', 'base2@example.com');

CREATE OR REPLACE MASKING POLICY POLSHARE_LIB_DB.POLICIES.MP_BASE_ALWAYS
  AS (val STRING) RETURNS STRING -> '***BASE_ALWAYS***';
-- Blocklist type (to confirm that raw values are visible across a share even via a view)
CREATE OR REPLACE MASKING POLICY POLSHARE_LIB_DB.POLICIES.MP_BASE_BLOCKLIST
  AS (val STRING) RETURNS STRING ->
  CASE WHEN CURRENT_ROLE() IN ('UNTRUSTED_ROLE') THEN '***MASKED***' ELSE val END;
CREATE OR REPLACE ROW ACCESS POLICY POLSHARE_LIB_DB.POLICIES.RAP_BASE_ACCT
  AS (region STRING) RETURNS BOOLEAN ->
  CURRENT_ACCOUNT_NAME() = 'PROVIDER_ACCT' OR region = 'APAC';

ALTER TABLE POLSHARE_LIB_DB.BASE.CUSTOMERS_BASE ALTER COLUMN ssn   SET MASKING POLICY POLSHARE_LIB_DB.POLICIES.MP_BASE_ALWAYS;
ALTER TABLE POLSHARE_LIB_DB.BASE.CUSTOMERS_BASE ALTER COLUMN email SET MASKING POLICY POLSHARE_LIB_DB.POLICIES.MP_BASE_BLOCKLIST;
ALTER TABLE POLSHARE_LIB_DB.BASE.CUSTOMERS_BASE ADD ROW ACCESS POLICY POLSHARE_LIB_DB.POLICIES.RAP_BASE_ACCT ON (region);

-- Case 1: secure view referencing a base table with policies (no policies on the view itself)
CREATE OR REPLACE SECURE VIEW POLSHARE_DB.S_VIEW.V_BASEPOL AS
SELECT id, region, ssn, email FROM POLSHARE_LIB_DB.BASE.CUSTOMERS_BASE;

-- Case 2: policies directly on view columns (base table has no policies)
CREATE OR REPLACE TABLE POLSHARE_LIB_DB.BASE.CUSTOMERS_PLAIN (id NUMBER, email STRING);
INSERT INTO POLSHARE_LIB_DB.BASE.CUSTOMERS_PLAIN VALUES
  (1, 'plain1@example.com'), (2, 'plain2@example.com');
CREATE OR REPLACE SECURE VIEW POLSHARE_DB.S_VIEW.V_VIEWPOL AS
SELECT id, email FROM POLSHARE_LIB_DB.BASE.CUSTOMERS_PLAIN;
CREATE OR REPLACE MASKING POLICY POLSHARE_DB.S_VIEW.MP_VIEWCOL_ALWAYS
  AS (val STRING) RETURNS STRING -> '***VIEWCOL***';
ALTER VIEW POLSHARE_DB.S_VIEW.V_VIEWPOL ALTER COLUMN email SET MASKING POLICY POLSHARE_DB.S_VIEW.MP_VIEWCOL_ALWAYS;

Where protection is applied Result via view
Always-on masking on base table Enforced (***BASE_ALWAYS***)
Row access policy on base table (account branching) Enforced (target rows only)
Blocklist type on base table (fail-open) Raw value leaks
Masking policy set directly on view column Enforced (***VIEWCOL***)

2026-09-01_16h23_58

Even if the base table is not shared, the policy evaluation context remains cross-share (CURRENT_ROLE() = NULL).

Cross-cutting Verification

Definition visibility: Consumers cannot view the policy definition body or actual names (SHOW command for DMP was not verified).

  • SHOW MASKING POLICIES / SHOW ROW ACCESS POLICIES / SHOW AGGREGATION POLICIES / SHOW PROJECTION POLICIES / SHOW JOIN POLICIES (all with IN DATABASE specified): all return 0 rows
  • DESCRIBE TABLE policy name column: Unknown Policy! (only the existence is visible)
  • GET_DDL('TABLE', ...): operation itself is unsupported in shared DBs (error 001131)

2026-09-01_16h26_28

Consumer-side application: Applying tags or policies to shared tables is blocked with 003001: Provider share does not have sufficient privileges.

2026-09-01_16h27_54

Limitations and Notes

  • This verification is for a same-organization, same-region, direct share configuration. Cross-organization, cross-region, via listing, and reader account configurations have not been verified.
  • Sharing of Dynamic Tables, materialized views, and external tables is not covered.
  • The behavior where database role branching becomes "determined per share" deviates from the official documentation (which states it is enabled by granting to a role on the consumer side). Re-verification before incorporating into a design is recommended.
  • Projection, aggregation, and join policies are features intended for trusted partners. The official documentation explicitly states that repeated inference attacks cannot be completely prevented.
  • Tag-based application of row access, projection, aggregation, and join policies is in Public Preview as of August 2026, so these were verified with direct application (tag-based masking and DMP tag-based application are GA).
  • DMP violation records can be delayed by up to 2 hours. Account for this delay when using for auditing.

Finally

The masking, row access, projection, aggregation, and join policies are all enforced across shares, and DMP only worked when applied via tags.
Ideally, sensitive data that doesn't need to be shared shouldn't be shared at all, but I think being able to use data protection policies when necessary is a benefit.

I hope this article is helpful to someone!


Snowflakeの導入支援はクラスメソッドに!

クラスメソッドでは Snowflake の導入を支援しております。
製品の詳細や支援の内容についてお気軽にお問い合わせください。

Snowflakeの詳細を見る

Share this article