Back to changelog
New
2 minute read

Snowflake: Row Access Policies

Atlas now manages Snowflake row access policies as a first-class resource. Declare the policy signature and body in HCL or SQL, and Atlas inspects, diffs, and migrates them with CREATE, ALTER, and DROP ROW ACCESS POLICY.

A row access policy in Snowflake is a schema-level object that returns a boolean per row, deciding which rows a query is allowed to see. Atlas now manages its full lifecycle as a first-class resource alongside your tables, views, and tags, in both the declarative and versioned workflows.

Enabling the Feature

Policy management with Atlas is opt-in. To enable it, set policies = true inside the mode "snowflake" block of your env to include row access policies in inspect, diff, and apply:

env "prod" {
url = "snowflake://user:pass@account_identifier/database"
dev = "snowflake://user:pass@dev_account_identifier/database"
schema {
src = "file://schema.hcl"
mode "snowflake" {
policies = true
}
}
}

Defining a Policy

Use the row_access_policy block to declare a policy in your HCL schema. The signature is declared as one arg block per argument, and the boolean expression as the as attribute; comment and tags are optional:

schema "public" {}
row_access_policy "sales_policy" {
schema = schema.public
arg "sales_region" {
type = VARCHAR(100)
}
as = "current_role() = 'ADMIN' or sales_region = 'US'"
comment = "Restrict rows by sales region"
}

Inspection works in the other direction, too. Atlas lists the policies in each schema and recovers their signature and body, so the policies already in your account round-trip into HCL with atlas schema inspect.

Generated SQL

Atlas generates CREATE, ALTER, and DROP ROW ACCESS POLICY statements based on the diff between your desired and current state:

-- Create row access policy "sales_policy"
CREATE ROW ACCESS POLICY "public"."sales_policy" AS ("sales_region" VARCHAR(100)) RETURNS BOOLEAN -> current_role() = 'ADMIN' or sales_region = 'US' COMMENT = 'Restrict rows by sales region';

A changed body, comment, or tag is handled in place with ALTER ROW ACCESS POLICY SET, without recreating the policy:

-- Modify row access policy "sales_policy"
ALTER ROW ACCESS POLICY "public"."sales_policy" SET BODY -> current_role() = 'ADMIN' or sales_region in ('US', 'EU');
-- Modify row access policy "sales_policy"
ALTER ROW ACCESS POLICY "public"."sales_policy" SET COMMENT = 'Restrict rows by sales region (NA + EU)';
Important: Snowflake does not allow changing the arguments of an existing policy with ALTER ROW ACCESS POLICY. When Atlas detects a change to the signature, such as an added argument or a different argument type, it recreates the policy with a DROP followed by a CREATE, rather than silently skipping the change:
-- Changing the signature requires a recreate (Snowflake limitation)
DROP ROW ACCESS POLICY "public"."sales_policy";
CREATE ROW ACCESS POLICY "public"."sales_policy" AS ("sales_region" VARCHAR(100), "dept" VARCHAR(100)) RETURNS BOOLEAN -> sales_region = 'US' and dept = 'SALES';

Every change carries its inverse. A CREATE reverses to DROP, and a SET BODY back to the previous body, so plans stay reversible and atlas migrate down works on them.

Policies That Query a Mapping Table

In addition to comparing against current_role(), a policy body looks the caller up in a mapping table. Snowflake resolves the body at CREATE time, so that table has to exist first. Atlas reads the body for the objects it names and orders the plan accordingly:

table "REGION_MAP" {
schema = schema.public
column "ROLE_NAME" {
type = VARCHAR(100)
}
column "REGION" {
type = VARCHAR(100)
}
}
row_access_policy "mapped_policy" {
schema = schema.public
arg "sales_region" {
type = VARCHAR(100)
}
as = "EXISTS (SELECT 1 FROM REGION_MAP WHERE ROLE_NAME = current_role() AND REGION = sales_region)"
}

The mapping table is created before the policy, without any annotation. For references the body parser cannot see, such as a policy that reaches through a UDF, declare them with depends_on.

Tags

Policies participate in Snowflake tagging like other schema objects, so a policy can be classified alongside the tables it protects:

tag "cost_center" {
schema = schema.public
}
row_access_policy "tagged_policy" {
schema = schema.public
as = "TRUE"
tag {
ref = tag.cost_center
value = "finance"
}
}

Tag changes are diffed and applied with ALTER ROW ACCESS POLICY ... SET TAG / UNSET TAG, like any other tagged resource. Tags are gated by their own mode flag, so add tags = true next to policies = true to manage them together:

mode "snowflake" {
policies = true
tags = true
}

Row access policies are available in the Snowflake driver today. See the row_access_policy reference for the full list of attributes, and Snowflake Database Security as Code for how policies fit next to the roles and grants that Atlas already manages.

featuresnowflakerow-access-policysecuritydeclarative