Snowflake RBAC and Secure Views in Practice
How Snowflake role hierarchies, grants, and secure views actually work in production: the role layout that scales, common future privilege traps, and how to audit who can see what.
Snowflake
This post was written by an engineer at QueryPlane. QueryPlane is an app builder for your database: bring your own postgres db and you can create interactive applications to share with other developers, coworkers or even your customers. If you’re interested in trying it out, get started here.
Snowflake ships with one of the more rigorous role-based access control systems of any cloud data warehouse, and the corollary is that almost every account that grows past a handful of users ends up tangled in it. The default ACCOUNTADMIN works fine for the first month. Then a new analyst joins, the data engineer adds a schema, finance asks for a read-only export of a single table, and someone notices that the marketing role can drop the customers table because grants drifted through SYSADMIN. The fix is not “be more careful with grants” — it is to design a role hierarchy that makes the wrong grant inexpressible, then enforce it with secure views wherever the row- or column-level policy logic needs to be more than a simple filter.
This post walks through the role model production Snowflake accounts converge on, the grant patterns that scale, the FUTURE GRANTS and ownership traps that bite teams two years in, and where secure views fit alongside masking and row access policies. It is the companion to our data masking and row access policies post — masking decides what values a role can see, RBAC decides what objects a role can touch, and together they cover the governance surface of a Snowflake account.
In this post, we’ll cover:
- The two role hierarchies — system roles, custom functional roles, and access roles
- Access roles vs functional roles — the pattern that keeps grants sane at 100+ users
GRANTmechanics —ALL PRIVILEGES,OWNERSHIP, future grants, and the inheritance rulesSHOW GRANTS— every variant and what each one actually tells you- Secure views — when to reach for them over masking or row access policies
- Common traps —
MANAGED ACCESSschemas, ownership drift, and warehouse-grant fan-out - Auditing —
GRANTS_TO_ROLES,GRANTS_TO_USERS,ACCESS_HISTORY, and the queries to know
The system roles
Snowflake creates five system roles when you provision the account and they do not behave like other roles. You cannot drop them, you cannot revoke ownership of them, and three of them sit at the top of the role hierarchy by default.
ACCOUNTADMIN is the superuser. It owns the account, can grant any privilege on any object, and is the role that signs up for the trial. Almost every “I locked myself out” Snowflake incident comes from misusing it — for example, creating a database while logged in as ACCOUNTADMIN, which makes ACCOUNTADMIN the owner of the database, then revoking your own access to it. The right move on day one is to grant ACCOUNTADMIN to one or two named users, log in to those users only for break-glass operations, and do all routine work under lower-privileged roles.
SECURITYADMIN and USERADMIN handle identity and access. USERADMIN can create roles and users; SECURITYADMIN can manage all grants in the account. They sit below ACCOUNTADMIN and above the rest of the hierarchy. In practice the data platform team operates from SECURITYADMIN for grant work, and almost no one logs in as USERADMIN directly.
SYSADMIN owns most of the actual data — databases, schemas, tables. This is the role under which all custom roles should be grafted. Snowflake’s recommended pattern is that every custom role is granted to SYSADMIN, so SYSADMIN retains visibility into every privilege that exists below it. If you skip this step, an account admin logging in as SYSADMIN to debug a permission issue cannot see the grant chain, and the only role with that visibility is SECURITYADMIN — which has no USAGE on the data.
PUBLIC is granted to every user automatically. Treat it as untrusted. Any grant on PUBLIC is effectively a grant to every login in the account, including service accounts. The first hardening pass on a new Snowflake account is usually to audit PUBLIC for unintended grants and revoke them.
The two-tier role pattern
The role layout that scales to a hundred or a thousand users is a two-tier separation between access roles (what objects you can touch) and functional roles (what job you do). It is the layout Snowflake documents as best practice and it is the one almost every mature account converges on, even if they started flat and refactored two years in.
Access roles describe a permission on a specific resource. One access role per data object scope per privilege bundle. Names look like DB_ANALYTICS__READ, DB_ANALYTICS__WRITE, DB_ANALYTICS_SCHEMA_FINANCE__READ. The access role owns the grants — GRANT SELECT ON ALL TABLES IN SCHEMA analytics.finance TO ROLE DB_ANALYTICS_SCHEMA_FINANCE__READ — but is not granted to users.
Functional roles describe a job — ANALYST, DATA_ENGINEER, FINANCE_ANALYST, LOOKER_SERVICE. They are granted to users, but they never own grants on data objects. Their privileges come from GRANT ROLE DB_ANALYTICS_SCHEMA_FINANCE__READ TO ROLE FINANCE_ANALYST. Every privilege a functional role has comes from rolling up a set of access roles.
The reason this separation matters: people change jobs, schemas get added, and the set of objects an analyst can read changes every quarter. With flat grants, that change involves running dozens of revoke/grant statements on the ANALYST role itself. With the two-tier pattern, you either grant an existing access role to the functional role (GRANT ROLE DB_MARKETING_SCHEMA_CAMPAIGNS__READ TO ROLE ANALYST) or revoke one. The functional roles are stable. The access roles are stable. Only the mapping between them changes.
-- Access role: read access to a schema
USE ROLE SECURITYADMIN;
CREATE ROLE DB_ANALYTICS_SCHEMA_FINANCE__READ;
USE ROLE SYSADMIN;
GRANT USAGE ON DATABASE analytics TO ROLE DB_ANALYTICS_SCHEMA_FINANCE__READ;
GRANT USAGE ON SCHEMA analytics.finance TO ROLE DB_ANALYTICS_SCHEMA_FINANCE__READ;
GRANT SELECT ON ALL TABLES IN SCHEMA analytics.finance
TO ROLE DB_ANALYTICS_SCHEMA_FINANCE__READ;
GRANT SELECT ON FUTURE TABLES IN SCHEMA analytics.finance
TO ROLE DB_ANALYTICS_SCHEMA_FINANCE__READ;
-- Functional role: granted to users
USE ROLE SECURITYADMIN;
CREATE ROLE FINANCE_ANALYST;
GRANT ROLE DB_ANALYTICS_SCHEMA_FINANCE__READ TO ROLE FINANCE_ANALYST;
GRANT ROLE FINANCE_ANALYST TO ROLE SYSADMIN;
GRANT ROLE FINANCE_ANALYST TO USER alice;
Two things in that snippet are worth flagging. First, SECURITYADMIN creates roles and grants role-to-role and role-to-user edges; SYSADMIN grants object privileges. Splitting the work keeps each grant statement runnable by the role with the narrowest authority that can do it. Second, the FINANCE_ANALYST role is grafted onto SYSADMIN (GRANT ROLE FINANCE_ANALYST TO ROLE SYSADMIN) — that is the line that keeps SYSADMIN aware of every privilege below it.
Future grants
Tables get added every week. Schemas grow. If your access roles only carry GRANT SELECT ON ALL TABLES IN SCHEMA …, the role works today and silently breaks the moment someone runs CREATE TABLE. The new table is owned by whichever role created it and the existing access role has no privilege on it.
Future grants close this gap. They are grants on a schema or database that take effect on objects of a given type that do not yet exist — when the object is later created, Snowflake applies the future grant automatically.
GRANT SELECT ON FUTURE TABLES IN SCHEMA analytics.finance
TO ROLE DB_ANALYTICS_SCHEMA_FINANCE__READ;
GRANT SELECT ON FUTURE VIEWS IN SCHEMA analytics.finance
TO ROLE DB_ANALYTICS_SCHEMA_FINANCE__READ;
The rule of thumb: every grant on ALL TABLES IN SCHEMA should be paired with a grant on FUTURE TABLES IN SCHEMA for the same object type. The first covers what exists; the second covers what will exist. The same applies to ALL VIEWS, ALL EXTERNAL TABLES, ALL STAGES, ALL PROCEDURES, and the other object-type variants.
Two subtleties trip teams up. Future grants are not inherited from database to schema — a future grant at the database level applies only to schemas in the database, not to tables in those schemas. To cover tables across all schemas you grant on FUTURE TABLES IN DATABASE. Second, schema-level future grants take precedence over database-level ones, so a schema-level future grant on the same object type overrides the database-level one. That last rule is worth knowing because it shows up in audits as “I granted at the database level but new tables in schema X don’t show up to this role” — usually because someone later granted at the schema level for a different role and overrode the inherited rule.
Inheritance and USE SECONDARY ROLES
Role-to-role grants form a graph, not a tree. ANALYST can be granted to SENIOR_ANALYST, and SENIOR_ANALYST can be granted to DATA_LEAD. A user with DATA_LEAD activated for the session can also use any privilege granted to SENIOR_ANALYST or ANALYST, transitively. This is the standard inheritance rule and is what lets functional roles roll up access roles cleanly.
The behavior changes when a user is granted multiple unrelated functional roles. By default only one role is active per session — the one set by USE ROLE or the user’s default — and the others are inert. Snowflake added secondary roles to allow combining privileges from multiple branches of the role graph in the same session:
USE ROLE FINANCE_ANALYST;
USE SECONDARY ROLES ALL;
After USE SECONDARY ROLES ALL, the session has every privilege available to every role granted to the user, regardless of which primary role is active. That sounds convenient and is the default in Snowsight since 2022, but it has a real cost: the principle of least privilege is harder to reason about, and masking and row access policies that check CURRENT_ROLE() see only the primary role, not the union. A user logged in as ANALYST with SECURITYADMIN as a secondary role can still read a column whose mask checks CURRENT_ROLE() = 'SECURITYADMIN', but the mask sees ANALYST and applies the analyst-tier rule.
For production accounts the cleanest default is to leave secondary roles off and let users USE ROLE to switch deliberately. For ad-hoc analysis it is fine to opt in.
See what QueryPlane can build for you
Connect to your database, write SQL with AI, and build shareable apps — all from your browser.
Ownership and MANAGED ACCESS
Every Snowflake object has exactly one owner role. The owner can grant any privilege on the object to any other role, alter the object, and drop it. By default, an object is owned by whichever role created it. That default is the source of most “we can’t deploy” outages.
The drift looks like this: a data engineer logs in as ACCOUNTADMIN to debug a one-off issue, creates a table to test a hypothesis, forgets about it. The table is now owned by ACCOUNTADMIN. Six months later the access role that grants read on the schema includes the table — but the access role’s GRANT SELECT ON ALL TABLES IN SCHEMA doesn’t actually apply, because future grants at the schema level only fire on creation and ALL TABLES only fires when the access role is granted. The analyst can’t read the table. The data engineer who can fix it left two months ago.
Two patches keep this manageable. The first is to grant OWNERSHIP on every new object back to a single owning role — typically the access-role-owning role for that schema — using GRANT OWNERSHIP ON TABLE … TO ROLE … REVOKE CURRENT GRANTS. The second is to enable managed-access schemas:
CREATE SCHEMA analytics.finance WITH MANAGED ACCESS;
In a managed-access schema, only the schema owner can grant privileges on objects in the schema. An object owner — say, the role that created a table inside the schema — cannot grant SELECT on it to a third role. Grants must be made by the schema owner. That single constraint stops most ownership drift in its tracks, because the only way to make an object readable is for the schema owner to do it, and the schema owner is the access-role-owning role you already designed around.
Secure views
A standard view in Snowflake is a saved SQL query that any user with SELECT on the view can read. Snowflake’s optimizer is free to push predicates from outside the view into the underlying tables, which is what makes views fast — but it also means that, in pathological cases, an attacker who can write a clever-enough WHERE clause on the view can infer information about rows the view filters out.
The classic example: a view filters orders to the current user’s customer, but the optimizer pushes a user-supplied predicate down to the base table. A predicate that triggers a divide-by-zero error on one specific row reveals the existence of that row in the error message, even though the view should have hidden it. Inference attacks like this are rare in practice but extremely hard to prove the absence of, which is why Snowflake offers a hardened variant.
A secure view is a view created with the SECURE keyword. Three things change. The view definition is hidden from non-owners — GET_DDL returns nothing useful, INFORMATION_SCHEMA.VIEWS shows the view but not its body. The optimizer treats the view as a black box: predicates from the outer query are not pushed down into the view body where they could leak information through plan shapes or errors. And functions like internal-only UDFs that reference user-supplied state are blocked from inference.
CREATE OR REPLACE SECURE VIEW analytics.public_orders AS
SELECT
order_id,
order_date,
total_amount
FROM analytics.finance.orders
WHERE customer_id = CURRENT_USER_CUSTOMER_ID();
The performance cost is real. Secure views can be 1.5x to 3x slower than the equivalent standard view because the optimizer cannot use the same set of rewrites. Use them when one of three conditions applies: the view is the only access path to sensitive data and you cannot afford an inference leak; the view body contains business logic you do not want to expose to consumers; or the view is shared across a Snowflake Data Share where consumers are explicitly untrusted. For internal analyst-facing views on already-policy-protected tables, a standard view is usually the right call.
Secure views vs masking vs row access policies
The three governance primitives overlap and the choice between them comes up often enough to memorize.
Masking policies are the right answer for column-level rules. The rule “show email to role A, mask it for everyone else” lives entirely on the column. The same policy attaches to every table that has an email column. There is no view to maintain.
Row access policies are the right answer for row-level rules whose logic fits in a boolean expression — region = CURRENT_REGION_FOR_ROLE(), or customer_id IN (SELECT customer_id FROM session_mapping WHERE role = CURRENT_ROLE()). The policy attaches to the table; queries see filtered rows transparently.
Secure views are the right answer when the rule involves more than a column-level mask and more than a single boolean — a custom join, a column-list change, a UNION of base tables based on the calling role, or any logic that needs the full power of SQL. They are also the right answer when you need to hide the query body itself, which masking and row access policies do not.
In a well-designed Snowflake account, masking and row access policies cover 80% of governance and the remaining 20% lives in secure views layered on top.
SHOW GRANTS — every variant
SHOW GRANTS is the standard tool for understanding who can do what. It has a half-dozen variants and the differences matter in practice.
-- Privileges granted directly to a role (does NOT include inherited)
SHOW GRANTS TO ROLE ANALYST;
-- All roles granted to a user (and their immediate parents)
SHOW GRANTS TO USER alice;
-- Roles granted to a role (and roles granted to those roles via inheritance)
SHOW GRANTS OF ROLE ANALYST;
-- Privileges on a specific object
SHOW GRANTS ON TABLE analytics.finance.orders;
-- The role that owns a specific object
SHOW GRANTS ON SCHEMA analytics.finance;
-- Future grants on a schema
SHOW FUTURE GRANTS IN SCHEMA analytics.finance;
-- Future grants on a database
SHOW FUTURE GRANTS IN DATABASE analytics;
The trap most users hit first: SHOW GRANTS TO ROLE ANALYST does not show inherited privileges. If ANALYST was granted DB_ANALYTICS__READ, the output lists that role grant, not the underlying SELECT privileges that DB_ANALYTICS__READ carries. To see the full effective set, you need to walk the graph yourself or query ACCOUNT_USAGE.GRANTS_TO_ROLES, which materializes the role-to-role and role-to-privilege edges.
-- Every direct privilege held by ANALYST or any role it inherits
WITH RECURSIVE role_graph AS (
SELECT name AS role_name FROM SNOWFLAKE.ACCOUNT_USAGE.ROLES
WHERE name = 'ANALYST'
UNION ALL
SELECT g.name
FROM SNOWFLAKE.ACCOUNT_USAGE.GRANTS_TO_ROLES g
JOIN role_graph r ON g.grantee_name = r.role_name
WHERE g.granted_on = 'ROLE' AND g.deleted_on IS NULL
)
SELECT DISTINCT
g.privilege,
g.granted_on,
g.name AS object_name,
g.grantee_name AS via_role
FROM SNOWFLAKE.ACCOUNT_USAGE.GRANTS_TO_ROLES g
JOIN role_graph r ON g.grantee_name = r.role_name
WHERE g.deleted_on IS NULL
AND g.granted_on != 'ROLE';
That query is the answer to “what can this role actually do.” It is also the right starting point for every quarterly access review.
Auditing — who actually used what
Grants describe what is possible; ACCESS_HISTORY describes what happened. The view records every column-level read and write across the account, with the user, role, object, and query that touched it. There is up to a 3-hour latency on the data, but it is the single most useful artifact for answering “did anyone actually read PII in the last 90 days, and which roles.”
-- Every query in the last 7 days that touched the orders.email column
SELECT DISTINCT
ah.query_id,
ah.user_name,
ah.role_name,
ah.query_start_time
FROM SNOWFLAKE.ACCOUNT_USAGE.ACCESS_HISTORY ah,
LATERAL FLATTEN(input => ah.base_objects_accessed) obj,
LATERAL FLATTEN(input => obj.value:columns) col
WHERE ah.query_start_time >= CURRENT_DATE - 7
AND obj.value:objectName::STRING ILIKE '%ORDERS%'
AND col.value:columnName::STRING = 'EMAIL';
Two patterns recur. The first is the unused-role check: filter ACCESS_HISTORY to the last 90 days, count distinct queries by role_name, and any role that appears zero times is a candidate for revocation. The second is the privilege escalation check: find every query where the active role was ACCOUNTADMIN or SECURITYADMIN and confirm each one corresponds to a known operator action. Routine analytical queries should never be running under those roles, and a sudden uptick is a strong signal of break-glass usage that should be reviewed.
Common pitfalls
Granting OWNERSHIP instead of ALL PRIVILEGES. GRANT OWNERSHIP transfers the object to a new role; the previous owner loses the ability to do anything with it. Teams sometimes use GRANT OWNERSHIP thinking they are granting all privileges. The right command for “give this role full control without changing the owner” is GRANT ALL PRIVILEGES. The right command for “transfer this object to a new owner role” is GRANT OWNERSHIP … COPY CURRENT GRANTS — and the COPY CURRENT GRANTS clause is what preserves existing grants on the object across the ownership change.
Forgetting USAGE on the database or schema. A user can have SELECT on every table in analytics.finance and still see “object does not exist or not authorized” if they lack USAGE on analytics or analytics.finance. The error message is the same regardless of which privilege is missing, which is one of the more frustrating things about debugging Snowflake permissions. Always grant USAGE on the database and schema as part of the same access-role definition that grants SELECT on tables.
Warehouse-grant fan-out. Every functional role needs USAGE on a warehouse to run queries, and warehouse credits are the dominant cost. Granting USAGE on a single shared COMPUTE_WH warehouse to every role is convenient but means every role contends for the same compute and there is no way to budget by team. The typical fix is to create per-team warehouses (FINANCE_WH, MARKETING_WH, ENGINEERING_WH) and grant USAGE only to the access roles that should bill against them. Resource monitors then cap per-team spend cleanly.
Service accounts with ACCOUNTADMIN. The first integration usually gets set up under ACCOUNTADMIN because it works on the first try. Every integration after that inherits the pattern. The fix is to create a SERVICE_ROLE_<TOOL> per integration with the narrowest set of privileges the tool actually needs, and rotate the service account credentials separately from human user credentials. Looker should not be able to drop your tables. Neither should dbt Cloud, in most setups — only the schemas it owns.
Future grants on the wrong target. GRANT … ON FUTURE TABLES IN DATABASE covers tables created in any schema under the database, but only when no schema-level future grant overrides it. Teams sometimes set up database-level future grants, then later add a schema-level future grant for a different role, and find that the database-level rule silently stopped applying. The fix is to standardize on one level — either database or schema — and use only that level for future grants in a given branch of the schema tree.
A worked example: onboarding a new schema
Putting the patterns above together, here is the end-to-end script for adding a new marketing.campaigns schema with read access for analysts and write access for marketing engineers.
-- 1. SECURITYADMIN creates the access roles
USE ROLE SECURITYADMIN;
CREATE ROLE DB_MARKETING_SCHEMA_CAMPAIGNS__READ;
CREATE ROLE DB_MARKETING_SCHEMA_CAMPAIGNS__WRITE;
-- 2. SYSADMIN creates the schema as managed-access
USE ROLE SYSADMIN;
CREATE SCHEMA marketing.campaigns WITH MANAGED ACCESS;
-- 3. SYSADMIN grants on the schema (object grants)
GRANT USAGE ON DATABASE marketing TO ROLE DB_MARKETING_SCHEMA_CAMPAIGNS__READ;
GRANT USAGE ON SCHEMA marketing.campaigns TO ROLE DB_MARKETING_SCHEMA_CAMPAIGNS__READ;
GRANT SELECT ON ALL TABLES IN SCHEMA marketing.campaigns
TO ROLE DB_MARKETING_SCHEMA_CAMPAIGNS__READ;
GRANT SELECT ON FUTURE TABLES IN SCHEMA marketing.campaigns
TO ROLE DB_MARKETING_SCHEMA_CAMPAIGNS__READ;
GRANT SELECT ON FUTURE VIEWS IN SCHEMA marketing.campaigns
TO ROLE DB_MARKETING_SCHEMA_CAMPAIGNS__READ;
GRANT USAGE ON DATABASE marketing TO ROLE DB_MARKETING_SCHEMA_CAMPAIGNS__WRITE;
GRANT USAGE ON SCHEMA marketing.campaigns TO ROLE DB_MARKETING_SCHEMA_CAMPAIGNS__WRITE;
GRANT CREATE TABLE, CREATE VIEW ON SCHEMA marketing.campaigns
TO ROLE DB_MARKETING_SCHEMA_CAMPAIGNS__WRITE;
GRANT INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA marketing.campaigns
TO ROLE DB_MARKETING_SCHEMA_CAMPAIGNS__WRITE;
GRANT INSERT, UPDATE, DELETE ON FUTURE TABLES IN SCHEMA marketing.campaigns
TO ROLE DB_MARKETING_SCHEMA_CAMPAIGNS__WRITE;
-- 4. SECURITYADMIN attaches access roles to functional roles
USE ROLE SECURITYADMIN;
GRANT ROLE DB_MARKETING_SCHEMA_CAMPAIGNS__READ TO ROLE ANALYST;
GRANT ROLE DB_MARKETING_SCHEMA_CAMPAIGNS__READ TO ROLE FINANCE_ANALYST;
GRANT ROLE DB_MARKETING_SCHEMA_CAMPAIGNS__WRITE TO ROLE MARKETING_ENGINEER;
-- 5. Make sure SYSADMIN can see what's below it
GRANT ROLE MARKETING_ENGINEER TO ROLE SYSADMIN;
-- 6. Apply masking on the new email column (assumes mask exists)
USE ROLE SECURITYADMIN;
ALTER TABLE marketing.campaigns.subscribers
MODIFY COLUMN email SET MASKING POLICY email_mask;
That sequence reuses every pattern from the earlier sections. The access roles encode the actual permissions on the schema. The functional roles consume them. Future grants cover tables that do not yet exist. Managed access prevents object owners from creating side-channel grants. And the masking policy on email keeps the column hidden from anyone whose role doesn’t unlock it — orthogonal to the RBAC grant model and complementary to it.
Wrapping up
The Snowflake RBAC model is bigger than it needs to be for the first ten users and exactly the right size by the time you have a hundred. The two-tier access-role / functional-role split is the structural decision that pays off most over time: it keeps grants stable as people change jobs and schemas grow, and it makes the answer to “what can this role see” computable from a single recursive query against GRANTS_TO_ROLES rather than a manual walk through dozens of edges. Add managed-access schemas to prevent ownership drift, pair every ALL TABLES grant with a FUTURE TABLES grant to prevent silent breakage, and reserve secure views for the cases where masking and row access policies cannot express the rule.
For the data governance picture, treat this post and the data masking and row access policies post together: RBAC decides what objects you can touch, masking and row access policies decide what values inside those objects you can see, and ACCESS_HISTORY is the audit trail across both. The combination is what an external auditor will ask about, and it is what makes Snowflake a defensible place to keep customer data.
If you operate Snowflake for a real workload, QueryPlane connects directly to your warehouse, runs every query under your own active role, and respects every policy and grant you have already configured — the same security boundary your snowsql and Snowsight sessions enforce, applied to a UI your team can share. For deeper Snowflake operational topics, see the warehouse sizing, query profile, clustering keys, dynamic tables, streams and tasks, and zero-copy clone guides.
Frequently asked questions
What is the difference between an access role and a functional role in Snowflake?
An access role owns object-level privileges (SELECT, INSERT, USAGE, OWNERSHIP) on databases, schemas, tables, views, and warehouses. It is never granted to a user. A functional role describes a job — ANALYST, DATA_ENGINEER, FINANCE_ANALYST — and is granted to users. Functional roles get their privileges by being granted access roles. The separation means that when an analyst changes teams you only swap which access roles are granted to which functional role, instead of revoking and re-granting object privileges directly.
Should every custom role be granted to SYSADMIN?
Yes, for almost every account. Snowflake’s recommended hierarchy puts every custom role under SYSADMIN so that the role with the broadest data ownership can also see every privilege below it. The exceptions are roles that intentionally exist outside the data-platform hierarchy — for example, a BILLING_ADMIN role that only manages account billing under ACCOUNTADMIN — but those are rare. If you skip this step, SYSADMIN cannot debug a permissions problem on objects it nominally owns, and grants drift toward ACCOUNTADMIN.
What is a future grant and when do I need one?
A future grant is a grant on a schema or database that takes effect on objects of a given type that do not yet exist. When you later run CREATE TABLE, Snowflake automatically applies the future grant to the new table. Without future grants, every new table requires a manual GRANT SELECT … TO ROLE … statement. The rule of thumb: every grant on ALL TABLES IN SCHEMA should be paired with a grant on FUTURE TABLES IN SCHEMA. Schema-level future grants take precedence over database-level future grants on the same object type, so pick one level per branch of your schema tree and stay there.
When should I use a secure view instead of a masking policy?
Use a masking policy when the rule is “transform the value of one column based on the calling role.” Use a secure view when the rule involves more than a column transformation — a join against a permission table, a different column list per role, a UNION of base tables, or any business logic that needs the full power of SQL. Secure views also hide the view body from non-owners, which masking policies do not, so they are the right choice when the query logic itself is sensitive. Expect a 1.5x to 3x performance cost on secure views relative to standard views; for purely column-level rules the masking policy is faster.
What is a managed-access schema?
A managed-access schema is a schema where only the schema owner can grant privileges on objects in the schema. Object owners (the role that created a given table) lose the default ability to grant SELECT on their own objects. The constraint prevents the most common form of grant drift: a role creates a table, then grants SELECT on it to a third role without the schema owner’s knowledge, and the data-platform team loses visibility into who can read what. Create new schemas with WITH MANAGED ACCESS unless you have a specific reason not to.
How do I list every privilege a role actually has, including inherited ones?
SHOW GRANTS TO ROLE only returns direct grants — it does not walk the role inheritance graph. To get the full effective set, query SNOWFLAKE.ACCOUNT_USAGE.GRANTS_TO_ROLES with a recursive CTE that walks every role-to-role edge starting from the role you care about, then collects every non-role grant attached to any role in the result set. There is a 1-2 hour latency on the ACCOUNT_USAGE views, so for break-glass debugging you may need to walk SHOW GRANTS manually instead.
Why does my user see “object does not exist or not authorized” when I clearly granted SELECT?
The error is the same regardless of which privilege is missing, and the answer 90% of the time is missing USAGE on the containing database or schema. A grant of SELECT on a table is meaningless without USAGE on the schema, and USAGE on the schema is meaningless without USAGE on the database. Always grant all three in the same access-role definition. The other common cause is that the active role at query time is not the role you granted to — SHOW GRANTS TO USER shows which roles the user has, and SELECT CURRENT_ROLE() shows which one is active.
Should I use USE SECONDARY ROLES ALL?
By default, no, for production accounts. Secondary roles combine privileges from every role granted to a user, which is convenient but undermines least-privilege auditing — ACCESS_HISTORY shows the primary role, and masking policies that check CURRENT_ROLE() see only the primary, so the actual privilege used can be hard to reconstruct. Snowsight enables secondary roles by default since 2022, but for service accounts and for any role with sensitive write access the cleaner pattern is to leave secondary roles off and let users USE ROLE explicitly when they need to switch contexts. For ad-hoc analyst sessions where the cost of debugging “which role can do this” is low, opt-in is fine.
How do I audit who actually queried a sensitive table?
Query SNOWFLAKE.ACCOUNT_USAGE.ACCESS_HISTORY filtered to the object name in base_objects_accessed and the date range you care about. The view records every column-level access, with the user, role, and query that performed it, on up to a 3-hour latency. It is the single best tool for “who has read PII in the last 90 days.” For unused-role detection, group ACCESS_HISTORY by role_name over a 90-day window and any role with zero rows is a revocation candidate. For privilege-escalation detection, filter to role_name IN ('ACCOUNTADMIN', 'SECURITYADMIN') and confirm each query corresponds to a known break-glass action.