Snowflake Data Masking, Row Access & Privacy Policies
Sub-objective 2.2 lists three governance features that the exam treats as separate: dynamic data masking, row access policies, and privacy policies. They control what a querier actually sees in a result set. Masking hides values in a column. Row access policies remove whole rows. Privacy policies force queries to aggregate before they return anything. All three require Enterprise Edition, the same boundary that gated yesterday’s session policies on Day 22.
Today’s Concept
Micro-Concept 1: Dynamic Data Masking, Column-Level Control
A masking policy is a schema-level object that hides values in a single column at query time. The underlying data is never changed on disk. The mask is applied as the result set is built, based on who is running the query.
The policy body is a CASE expression that almost always tests CURRENT_ROLE(). Privileged roles see the real value. Everyone else sees a masked one. The policy receives the column value as its single argument and returns a value of the same type.
CREATE MASKING POLICY mask_ssn AS (val STRING) RETURNS STRING ->
CASE
WHEN CURRENT_ROLE() IN ('ACCOUNTADMIN') THEN val
ELSE '***-**-' || RIGHT(val, 4)
END;Two facts about masking policies are tested often. The policy is a schema-level object, so a database and schema must exist before you create one. And the data type in the policy signature has to match the column data type. A STRING policy cannot be applied to a NUMBER column.
Masking works on tables and views. When the masked column appears in a join, a filter, or a projection, the mask applies in every location. That is why a masked column can quietly change the result of a WHERE clause for an unprivileged role.
In training sessions I have run, the most common surprise is that masking is role-based, not user-based. The policy reads CURRENT_ROLE(), so a user inherits visibility from whichever role they have activated, not from their username.
Micro-Concept 2: Tag-Based Masking, One Policy for Many Columns
Tag-based masking combines object tagging with masking. You set a masking policy on a tag using ALTER TAG ... SET MASKING POLICY. Any column carrying that tag is then protected automatically, with no per-column ALTER TABLE needed.
The condition for automatic protection is a data-type match. When the tagged column’s type matches the masking policy signature, the column is masked. This is how one policy covers hundreds of columns across many tables.
A tag-based policy can read the tag’s string value inside its own body. The function for that is SYSTEM$GET_TAG_ON_CURRENT_COLUMN. That lets a single policy mask differently depending on the sensitivity label assigned to each column.
This is one of the harder distinctions in Domain 2. Plain masking attaches a policy to a column. Tag-based masking attaches a policy to a tag, and the column inherits it. The exam writes scenarios where tagging is the only scalable answer.
Micro-Concept 3: Row Access Policies, Row-Level Filtering
A row access policy is a schema-level object that decides whether a row is visible. It returns a boolean. Rows where the expression evaluates to true are returned. Rows where it evaluates to false are removed from the result, silently.
The policy has a signature: one or more column names with data types. Those columns feed the boolean expression. A simple policy compares them against CURRENT_ROLE() directly. A real one usually joins to a mapping table.
CREATE ROW ACCESS POLICY ra_region AS (region STRING) RETURNS BOOLEAN ->
'SALES_EXECUTIVE' = CURRENT_ROLE()
OR EXISTS (
SELECT 1 FROM region_map
WHERE role_name = CURRENT_ROLE()
AND allowed_region = region
);The mapping table pattern is the one the exam favours. A small table maps roles to the values they may see. The policy joins to it. Adding a new role to the mapping table changes access without altering the policy.
Three rules carry exam weight. A table column can be protected by only one row access policy at a time. An external table cannot be used as a mapping table. And you cannot change a policy’s signature with ALTER: you must drop and recreate it.
Micro-Concept 4: One Column, Not Both Signatures
This rule catches careful candidates. A single column cannot appear in both a masking policy signature and a row access policy signature at the same time. Snowflake rejects the second assignment.
You can still protect one table with both feature types. The masking policy guards column ssn. The row access policy keys on column region. The constraint is per-column, not per-table. The same column cannot be the input to both.
When a query hits a table that has both kinds of policy, the order is fixed. The row access policy runs first, filtering rows. The masking policy runs after, on the surviving rows. Knowing the order explains why a masked column can still drive row filtering elsewhere.
Micro-Concept 5: Privacy Policies, Aggregation and Projection
The third feature family is privacy policies. These are newer than masking and row access. They reached general availability in May 2024. The C03 outline lists them under sub-objective 2.2 as a distinct item, separate from masking.
An aggregation policy forces queries to aggregate before they return data. It sets a MIN_GROUP_SIZE. A group smaller than that returns no individual rows. This stops an analyst from reading one person’s record while still allowing group statistics.
CREATE AGGREGATION POLICY agg_min5 AS () RETURNS AGGREGATION_CONSTRAINT ->
AGGREGATION_CONSTRAINT(MIN_GROUP_SIZE => 5);A projection policy is the other half. It prevents a column from being projected in a SELECT list. The body returns a PROJECTION_CONSTRAINT. Set ALLOW => false and the blocked role cannot pull that column into output, even though row filtering and masking may permit other access.
The distinction the exam tests is intent. Masking changes the value an unprivileged role sees. Row access removes rows. Aggregation blocks per-record reads and demands group queries. Projection blocks a column from appearing in output at all. Four different protections, four different result-set effects.
All of these privacy policies require Enterprise Edition, the same floor as masking and row access. Account-level access control through RBAC, by contrast, is available in every edition. That edition split is itself a question, covered in Q1 below.
Cheat Sheet
| Topic | What to remember | Exam keyword |
|---|---|---|
| Masking policy basics | Schema-level object. Hides a column value at query time. Reads CURRENT_ROLE() | “Column-level, role-based” |
| Masking type match | Policy signature data type must match the column data type | “Type must match” |
| Tag-based masking | Set the policy on a tag. Tagged columns inherit it automatically. One policy, many columns | “Policy on a tag” |
| Row access policy | Returns BOOLEAN. True rows returned, false rows removed. Usually joins a mapping table | “Row-level filter” |
| Mapping table | Maps roles to permitted values. Update the table, not the policy. No external tables | “Mapping table lookup” |
| Both-signature rule | One column cannot be in both a masking AND a row access policy signature | “Not both signatures” |
| Evaluation order | Row access policy runs first, then masking policy | “Row access first” |
| Aggregation policy | Forces queries to aggregate. MIN_GROUP_SIZE blocks small groups | “MIN_GROUP_SIZE” |
| Projection policy | Blocks a column from appearing in SELECT output. ALLOW => false | “Cannot project column” |
| Edition floor | Masking, row access, and privacy policies all require Enterprise+ | “Enterprise minimum” |
| Available all editions | Object-level access control (RBAC) works in Standard too | “RBAC everywhere” |
Exam Tip
The exam loves to confuse the four protections. Match the requirement to the feature.
Match the verb in the stem. “Hide a value” or “show partial data” is masking. “Show only the rows for this role” is a row access policy. “Allow group statistics but no individual records” is an aggregation policy. “Stop this column from appearing in output” is a projection policy. When the stem names a single column to obscure, it is masking, not row access.
The both-signatures trap. A scenario applies a masking policy to a column, then tries to use that same column in a row access policy signature. The DDL fails. The same column cannot feed both signatures. Pick the answer that says the assignment is rejected, not the one that says both apply.
The edition trap. A question asks which feature works in Standard Edition. Masking, row access, tagging, and privacy policies are all Enterprise or higher. The only listed feature that works in every edition is object-level access control through RBAC. This is the documented answer to a Domain 2 sample-style question.
Hands-On Lab
lab_xs warehouse from Day 1. Run as ACCOUNTADMIN unless a step says otherwise.Set up a lab table with sensitive columns.
USE ROLE ACCOUNTADMIN;
USE WAREHOUSE lab_xs;
CREATE DATABASE IF NOT EXISTS day23_lab;
CREATE SCHEMA IF NOT EXISTS day23_lab.gov;
USE SCHEMA day23_lab.gov;
CREATE OR REPLACE TABLE customers_lab (
id INT, region STRING, name STRING, email STRING, ssn STRING);
INSERT INTO customers_lab VALUES
(1, 'EAST', 'Alice', '[email protected]', '123-45-6789'),
(2, 'WEST', 'Bob', '[email protected]', '987-65-4321'),
(3, 'EAST', 'Carol', '[email protected]', '555-11-2222');
SELECT * FROM customers_lab;ssn values. This is the baseline. Every protection added below changes what a less-privileged role would see, not what the owner sees.Create and apply a masking policy on ssn.
CREATE OR REPLACE MASKING POLICY mask_ssn AS (val STRING) RETURNS STRING ->
CASE
WHEN CURRENT_ROLE() IN ('ACCOUNTADMIN') THEN val
ELSE '***-**-' || RIGHT(val, 4)
END;
ALTER TABLE customers_lab MODIFY COLUMN ssn SET MASKING POLICY mask_ssn;
SELECT id, name, ssn FROM customers_lab;ssn stays in plain text, because the policy whitelists that role. The data on disk did not change. Only the query-time view changes for roles outside the whitelist.Prove masking is role-based. Create a low-privilege role and view the same column.
CREATE ROLE IF NOT EXISTS day23_analyst;
GRANT USAGE ON DATABASE day23_lab TO ROLE day23_analyst;
GRANT USAGE ON SCHEMA day23_lab.gov TO ROLE day23_analyst;
GRANT SELECT ON TABLE day23_lab.gov.customers_lab TO ROLE day23_analyst;
GRANT USAGE ON WAREHOUSE lab_xs TO ROLE day23_analyst;
GRANT ROLE day23_analyst TO ROLE ACCOUNTADMIN;
USE ROLE day23_analyst;
SELECT id, name, ssn FROM day23_lab.gov.customers_lab;
USE ROLE ACCOUNTADMIN;day23_analyst the ssn shows as ***-**-6789 and so on. The mask depends entirely on the active role. Switch the role, change the view. That is what “role-based, not user-based” means in practice.Add a row access policy on a different column. Region, not ssn, keeps the both-signatures rule satisfied.
CREATE OR REPLACE ROW ACCESS POLICY ra_region AS (region STRING) RETURNS BOOLEAN ->
CURRENT_ROLE() = 'ACCOUNTADMIN'
OR region = 'EAST';
ALTER TABLE customers_lab ADD ROW ACCESS POLICY ra_region ON (region);
USE ROLE day23_analyst;
SELECT id, name, region FROM day23_lab.gov.customers_lab;
USE ROLE ACCOUNTADMIN;Trigger the both-signatures rejection. Try to put ssn into a row access policy too.
CREATE OR REPLACE ROW ACCESS POLICY ra_ssn AS (ssn STRING) RETURNS BOOLEAN ->
TRUE;
-- This WILL fail: ssn already has a masking policy
ALTER TABLE customers_lab ADD ROW ACCESS POLICY ra_ssn ON (ssn);
-- Expected error: a column cannot be in a masking policy
-- signature and a row access policy signature at once.ssn column is already an input to mask_ssn. It cannot also be a row access policy signature column. This is the exact rule a scenario question tests. The fix is to key the row access policy on a non-masked column.Create an aggregation policy (privacy). Force group queries on a separate table.
CREATE OR REPLACE TABLE survey_lab (region STRING, score INT);
INSERT INTO survey_lab VALUES
('EAST',5),('EAST',7),('EAST',6),('EAST',8),('EAST',9),
('WEST',4),('WEST',3);
CREATE OR REPLACE AGGREGATION POLICY agg_min5
AS () RETURNS AGGREGATION_CONSTRAINT ->
CASE
WHEN CURRENT_ROLE() = 'ACCOUNTADMIN' THEN NO_AGGREGATION_CONSTRAINT()
ELSE AGGREGATION_CONSTRAINT(MIN_GROUP_SIZE => 5)
END;
ALTER TABLE survey_lab SET AGGREGATION POLICY agg_min5;day23_analyst, a grouped query on EAST (5 rows) returns a value. WEST has only 2 rows, so it falls below the minimum. Those rows collapse into a NULL remainder group. ACCOUNTADMIN bypasses the constraint. This is differential privacy in miniature: group stats yes, individual records no.Cleanup. Detach policies before dropping objects, then drop the lab database.
USE ROLE ACCOUNTADMIN;
USE SCHEMA day23_lab.gov;
ALTER TABLE customers_lab DROP ROW ACCESS POLICY ra_region;
ALTER TABLE customers_lab MODIFY COLUMN ssn UNSET MASKING POLICY;
ALTER TABLE survey_lab UNSET AGGREGATION POLICY;
DROP ROW ACCESS POLICY IF EXISTS ra_region;
DROP ROW ACCESS POLICY IF EXISTS ra_ssn;
DROP MASKING POLICY IF EXISTS mask_ssn;
DROP AGGREGATION POLICY IF EXISTS agg_min5;
DROP DATABASE IF EXISTS day23_lab;
DROP ROLE IF EXISTS day23_analyst;
-- Verify lab_xs survives for tomorrow
SHOW WAREHOUSES LIKE 'LAB_XS';DROP DATABASE removes the tables and any still-attached policies inside it. The day10_orders table from Day 10 is untouched. lab_xs stays for tomorrow.Snowflake Documentation
Official docs for today’s topics. The exam pulls directly from these.
External References
Community resources to reinforce today’s concepts.
Practice Questions
Options:
A. Data masking policies
B. Object-level access control
C. Object tagging
D. Customer-managed encryption keys
Why B: Object-level access control is RBAC on databases, schemas, tables, and other objects. It is a foundational feature present in every edition, including Standard.
Why not A: Masking policies require Enterprise Edition or higher. Standard accounts cannot create them.
Why not C: Object tagging is also an Enterprise+ governance feature. Day 24 covers it directly.
Why not D: Customer-managed keys (Tri-Secret Secure) require Business Critical Edition, an even higher floor than Enterprise.
Options:
A. A dynamic data masking policy on the country column
B. A row access policy using a mapping table of roles to regions
C. A projection policy on every sensitive column
D. An aggregation policy with a minimum group size
Why B: The requirement is row-level filtering by role. A row access policy returns true for permitted rows and removes the rest. A mapping table connects each role to its allowed regions.
Why not A: Masking hides values inside a column. It does not remove rows. The analysts would still see every row, just with a masked country.
Why not C: A projection policy blocks a column from being selected at all. It does not filter rows by region.
Why not D: An aggregation policy forces group queries. It does not give per-row visibility based on region, and it blocks the individual-row reads the requirement allows.
Options:
A. The same column can be used in both a masking policy signature and a row access policy signature
B. When a table has both policy types, the row access policy is evaluated before the masking policy
C. Masking policies physically alter the stored data in the table
D. A masking policy signature data type must match the protected column’s data type
E. Row access policies can use an external table as their mapping table
Why B: The order is fixed. Row filtering happens first, then masking applies to the surviving rows. Today’s lab Step 4 shows this directly.
Why D: The policy signature declares an input type. It must match the column type, or the assignment is rejected.
Why not A: A single column cannot appear in both signatures at once. This is the both-signatures rule from Micro-Concept 4.
Why not C: Masking is applied at query time. The data on disk never changes. This is the difference from static masking.
Why not E: Snowflake does not support an external table as a row access policy mapping table.
Options:
A. Apply a separate masking policy to each column with ALTER TABLE
B. Set a masking policy on a tag, then assign that tag to the PII columns
C. Create one row access policy that covers all the PII columns
D. Use a projection policy on the schema
Why B: Tag-based masking sets the policy on a tag using ALTER TAG. Any column carrying that tag, with a matching data type, is masked automatically. New tagged columns inherit protection without per-column DDL.
Why not A: Per-column assignment works but does not scale and does not auto-protect new columns. It is the manual approach tag-based masking replaces.
Why not C: Row access policies filter rows, not column values. They do not mask PII inside a column.
Why not D: A projection policy blocks a column from output entirely. It does not mask values for some roles while showing them to others.
Options:
A. An aggregation policy forces queries to aggregate and enforces a minimum group size
B. A projection policy changes the displayed value of a column for unprivileged roles
C. A projection policy can block a column from being projected in a SELECT list
D. Privacy policies are available in all Snowflake editions, including Standard
E. Aggregation policies remove individual rows the same way a row access policy does
Why A: An aggregation policy requires queries to return aggregated groups. The MIN_GROUP_SIZE argument sets the floor, below which no individual data is returned.
Why C: A projection policy with ALLOW => false stops a blocked role from pulling the column into output, even when other access is permitted.
Why not B: Changing a displayed value is masking, not projection. A projection policy either allows or blocks the column.
Why not D: Privacy policies require Enterprise Edition. Only RBAC works in every edition. Re-read Q1 if missed.
Why not E: An aggregation policy blocks per-record reads by demanding group queries. It does not filter rows by role the way a row access policy does.
Today you learned: Sub-objective 2.2 names three distinct governance features. A masking policy hides a column value at query time, based on CURRENT_ROLE(), and the policy signature type must match the column type. A row access policy returns a boolean to filter rows, usually against a mapping table. Privacy policies, namely aggregation and projection, force group queries or block columns from output.
One column cannot sit in both a masking policy signature and a row access policy signature. When a table has both, the row access policy runs first, then masking. Tag-based masking sets one policy on a tag so many columns inherit protection automatically.
All of these need Enterprise Edition or higher. Object-level access control through RBAC is the feature that works in every edition, the same RBAC foundation from Day 18.
Tomorrow (Day 24): Object Tagging, Data Classification, and Trust Center. Two of these are direct C03 sample-question topics. Data classification auto-detects PII and PHI columns. Trust Center evaluates your account against Snowflake’s security scanners. Tagging is the metadata layer that ties governance together, including the tag-based masking you met today.
