What tonight builds on the table
The table is still saltbox.customers: customer_id, name, email, phone, dob, national_id, region, plan, mrr, support_notes, with rows in EU, UK and APAC. The five jobs are still EU analyst, support agent, data scientist, finance analyst and APAC contractor. What changes tonight is that those five jobs stop being rows in a document and become five roles a person can actually hold, sitting on top of three roles that describe the data rather than the people.
★ What you walk out with tonight. Three access roles and five functional roles built and wired, verified against the design grid from session 1 rather than against a feeling. The direction privileges travel when a role is granted to a role, demonstrated on your own warehouse. A safe procedure for deleting a role nobody will admit to owning. And the measured reason this session matters: splitting one shared role five ways moves leaks from 70 to 22 and standing privileges from 17 to 1.
LiveOne layer of roles is where role sprawl comes from3 min▶
The natural first move is one role per person-shaped thing: analyst, then analyst_eu, then analyst_eu_finance, each granted directly on tables. It works for about a quarter. Then somebody joins who is almost an EU analyst but also needs revenue, and rather than reopen the model, a sixth role appears that is a copy of the fifth plus one grant.
What follows is mechanical. Every new table means visiting every role. Every job change means auditing which of thirty near-identical roles the person should now hold. Nobody deletes anything, because nobody can prove a role is unused. Within a year the role list is longer than the user list, and the reviewer, whose entire job is reading that list, has no chance.
Saltbox had 41 roles for 23 people. Six of them differed from another role by exactly one grant, and two were identical with different spellings. The list was not the result of anybody being careless; it was the result of a single-layer model meeting eighteen months of ordinary hiring.
Self-studyThe vocabulary for tonight3 min▶
- Access role - a role that holds privileges on objects and is granted to nothing but other roles. Named for the data it opens.
- Functional role - a role that represents a job. It holds no direct object privileges at all; it holds access roles. Named for the person doing the job.
- Role grant -
grant role a to role b. The privileges ofabecome available tob, which is the upward direction from session 1. - Re-pointing - changing which access roles a functional role holds. The single operation a reorg should require.
- PUBLIC - the role every user holds automatically. Anything granted here is granted to the entire future population of the account.
- Orphan role - a role with no members and no obvious purpose. Common, and the reason a safe deletion procedure is worth having.
The two-layer pattern
12 min liveTwo kinds of role, and the discipline is that they never mix. Access roles describe the data and are named for it. Functional roles describe jobs and are named for them. Users hold functional roles and nothing else, which is the rule that makes the whole thing reviewable.
LiveAccess roles: named for the data4 min▶
One access role per meaningful bundle of privileges on objects. Meaningful means a bundle that at least two jobs might plausibly want, or one job wants and the design treats as a unit. For Saltbox the design produces three.
ac_customers_read- the non-sensitive projection: customer_id, region, plan. Everybody who touches the table needs this and nothing about it is contentious.ac_customers_pii_read- name, email, phone and support_notes. One job needs it in clear. Every other job's relationship to this bundle is an argument that should happen once, here, rather than per table.ac_customers_finance_read- mrr. Not personal data, still restricted, and separated because the set of jobs that needs revenue is not the set that needs contact details.
Two properties make these roles do their job. They are granted to nothing but other roles, never to a user, so there is no path by which a person accumulates object privileges outside the model. And they are named for what they open, so the name survives the reorg that renames the team.
Do not create one access role per column. Ten columns produce ten roles, then thirty when the second table arrives, and you have rebuilt sprawl one layer down. Bundle by the classification tiers from session 1: non-sensitive, contact PII, highly sensitive, financial. If a bundle needs splitting later, splitting one access role is cheap because only the functional roles point at it.
LiveFunctional roles: named for the job4 min▶
One functional role per job in the written design, and the design already named five. A functional role holds access roles and holds no direct privileges on any object, which is the constraint that keeps the two layers from silently becoming one.
| Functional role | Holds | Because the design says |
|---|---|---|
fr_analyst_eu | ac_customers_read, ac_customers_finance_read | customer_id, region, plan, mrr in clear, EU rows only |
fr_support_agent | ac_customers_read, ac_customers_pii_read | contact details and support_notes in clear, EU and UK |
fr_data_scientist | ac_customers_read, ac_customers_finance_read | region, plan, mrr in clear, customer_id masked, all regions |
fr_finance | ac_customers_read, ac_customers_finance_read | customer_id, region, plan, mrr in clear, all regions |
fr_contractor_apac | ac_customers_read | region and plan only, APAC rows only |
Notice that three functional roles hold an identical pair of access roles and are still three roles. They differ on the row dimension, which session 3 handles, and on the masking of customer_id for the scientist, which session 4 handles. Collapsing them into one role now would make both of those sessions impossible to express, and it is exactly the collapse that produced the shared role the bench scores at 109 leaks.
A functional role with a direct object grant is a bug, and it is worth checking for. It usually arrives as an emergency: somebody needs one table today and creating an access role feels like ceremony. Six months later that grant is invisible, because the reviewer reads the access-role layer and this one is not in it.
LiveWhy two layers survive a reorg4 min▶
Reorgs are not an edge case, they are the weather. The two-layer model earns its keep because each of the three common changes touches exactly one thing.
- A job changes. The EU analyst picks up revenue responsibility. You grant one access role to one functional role. No object is re-granted, no user is touched, and every other job is untouched by construction.
- A new table arrives. You create one access role for it and grant it to the functional roles that need it. In a single-layer model this is a visit to every job-shaped role, which is where the copies come from.
- A person moves. Revoke one functional role, grant another. There is nothing else to check, because a person has never held anything else.
The cost is one extra layer of indirection and roughly three more roles. The saving is that the number of grant operations per change stops scaling with the size of the estate, which is the property that actually decides whether a model is still true in two years.
LiveThe naming convention is load-bearing4 min▶
Naming reads as cosmetic and is not, because a role list is the artifact a reviewer actually reads. They are not going to open every role and enumerate its grants; they are going to scan a list of names and stop on the ones that look wrong. That only works if a name carries its layer, its subject and its scope.
| Name | What a reviewer learns from it |
|---|---|
ac_customers_pii_read | Access layer, customers table, contact class, read only. Four facts, no clicks |
fr_contractor_apac | Functional layer, a contractor job, APAC scope. Immediately checkable against the design |
analyst2_new_FINAL | Nothing. It cannot be reviewed, cannot be safely deleted, and will outlive everybody who knows what it was |
temp_migration | That it was temporary at some point. No layer, no subject, no scope, no end date |
Three rules are enough. Prefix by layer, so ac_ and fr_ sort into two blocks and a functional role holding a direct grant becomes visible. Name access roles after the object and the class, never after the team that first asked. Name functional roles after the job, never after the person, because fr_marias_role is a role nobody will ever revoke.
Write the convention down next to the access design and enforce it in review. Naming conventions do not survive on taste; they survive because a pull request that breaks one gets a comment. The estate-wide version of this is cheaper than the alternative, which is a rename programme in year two touching every grant statement you have.
The operations that go wrong
6 min liveFour operations account for most of the damage done to a role model after it is built: granting a role to a role in the wrong direction, never deleting anything, granting to PUBLIC by reflex, and leaving the shared role in place because splitting it feels like admin. The last one is the expensive one, and the bench puts a number on it.
LiveGranting a role to a role, and which way the privileges travel4 min▶
Privileges inherit upward. Grant role A to role B and B holds A's privileges, not the other way round. The session 1 diagram covers the model; what matters here is the sentence you say to yourself before typing, because the statement reads naturally in the wrong direction.
grant role fr_support_agent to role fr_analyst_eu sounds like it gives support something. It gives the analyst everything support can see: name, email, phone and support_notes in clear, across both of support's regions. The analyst design asked for four non-sensitive columns. That one statement is the difference.
An engineer at Saltbox was asked to let the support lead see the analyst dashboards and typed the grant the way it reads in English. It ran without error, nobody's dashboard changed visibly, and it was found four months later by an access review looking for exactly this shape. The support lead had gained nothing. Every EU analyst had gained the contact columns.
The structural defence is the two-layer rule. Functional roles are not granted to each other, ever. When a job genuinely needs what another job has, the answer is to grant it the same access role, which is one extra keystroke and leaves a reviewable trace in the right layer. A functional-to-functional grant in a show grants output is then unambiguously a mistake rather than something that has to be reasoned about.
LiveNobody deletes a role, and the safe way to do it anyway4 min▶
Role lists only grow, and the reason is rational: dropping a role might break something invisible, and the reward for being right is nothing. So the orphans accumulate, and every one of them is a standing privilege waiting for somebody to be granted it by mistake.
The procedure below makes deletion boring enough to actually happen. It takes about a sprint of wall-clock time and roughly twenty minutes of work.
- Find the members.
show grants of rolelists the users and roles holding it. If it is genuinely empty, you are already most of the way there. - Find what it actually used. Query the account usage history for queries run under that role over the last 90 days. A role with members and no queries is dormant, which is a different and more interesting finding.
- Re-point. If it is in use, grant the members the correct functional role first. Nobody should lose access at the moment of deletion; that is what turns a cleanup into an incident.
- Revoke, do not drop. Revoke the role from every member and leave the role in place. It is now inert and instantly restorable.
- Wait a sprint. Two weeks catches the monthly report, the quarterly close job and the person who was on leave. This is the step everybody skips.
- Then drop it. With a commit message saying what it was, who held it and what they hold now, so the next person to find the gap has an answer.
Query history is evidence, not proof. A role with no queries in 90 days may serve an annual regulatory extract. Check the calendar as well as the log before dropping anything, and prefer the long revoke window over a clever argument.
LivePUBLIC and the accidental grant3 min▶
PUBLIC is granted automatically to every user, including the ones who have not been hired yet. A grant to PUBLIC is therefore not a grant to the current team; it is a standing commitment about everybody who will ever have an account, and it will not be re-examined when the contractor arrives in March.
It gets used because it is the fastest way to unblock somebody, and because a shared reference table genuinely does feel like it should be readable by all. Two things make it dangerous in a two-layer model specifically.
- It bypasses both layers. A privilege on PUBLIC is held by every functional role without appearing in any of their grant lists in a way a reviewer would notice, so the design and reality diverge silently.
- It is inherited into everything. PUBLIC sits at the bottom of the hierarchy, so its privileges travel up into every role above it, which is every role.
The workable position: allow PUBLIC to hold usage on the database and nothing else, and check for grants to it on a schedule. If a table really is readable by everyone, make an access role called ac_reference_read and grant it to every functional role. It is more typing and it is visible in the review, which is the entire difference.
LiveWhat the role split is actually worth3 min▶
Session 8 scores the finished policy set against 150 concrete access requests over five jobs, ten columns and three regions. The ladder starts from one shared analyst role and adds one control at a time. The row that matters for tonight is the fourth.
| Policies on | Leaks | Over-blocks | Standing privileges |
|---|---|---|---|
| One shared analyst role | 109 | 0 | 30 |
| + mask national_id for everybody | 94 | 0 | 25 |
| + mask email and phone, except support | 70 | 0 | 17 |
| + split the shared role five ways | 22 | 0 | 1 |
| + row access policy on region | 3 | 0 | 1 |
| + pseudonymise the modelling view | 0 | 0 | 0 |
Two masking policies, both real work, take leaks from 109 to 70. Splitting the shared role five ways takes them from 70 to 22 in one move, and takes standing privileges from 17 to 1. Nothing else on the ladder moves standing privileges at all, because a standing privilege is a column a role can read in clear that the design does not mark clear for that role, and while five jobs share one role every one of them is over-granted by definition.
That is the argument for spending tonight on roles rather than on something that demonstrates better. Role modelling does the most work and gets the least attention, because it reads as administration.
Self-studyHow many roles is too many4 min▶
The two-layer model adds roles on purpose, which raises the obvious question of where it stops being a model and starts being sprawl with better prefixes. Four signals are worth watching.
- Functional roles should track jobs, and jobs are countable. If you have more functional roles than distinct job descriptions, some of them are people, and people-shaped roles never get revoked.
- Access roles should track data bundles, not requests. An access role created for one request that one job made is a grant wearing a costume. Fold it into an existing bundle or accept that the bundle was wrong.
- Two roles with identical grants are one role. Worth a scheduled check, because these appear during migrations and nobody notices while both are in use.
- A role held by nobody for a quarter is a candidate. Not automatically wrong, but it goes on the list and gets the deletion procedure rather than being ignored for another year.
A useful sanity number: for one table with four classification tiers and five jobs, three to four access roles and five functional roles is right. If your first pass produces twenty, the bundles are too fine. If it produces two, at least one role is doing several jobs and the bench has already told you what that costs.
Build the five roles
★ 20 min · everyone buildsThe statements below are Snowflake's; the shape carries to the equivalents elsewhere. Run them against a scratch database rather than production, and keep the design grid from session 1 open next to you, because step seven is a diff against it.
Create the three access roles. Comments are not decoration here: the comment is what a reviewer reads when the name is not enough, and it is the only field that can carry the design decision.
Give each access role its column bundle. A grant is table-shaped rather than column-shaped, so a column subset is expressed as a view over the base table. Session 4 does the same job with masking policies on the table itself, which keeps one object instead of three; both are legitimate and the view is easier to read tonight.
Grant the objects to the access roles. Usage on the database and schema is needed on every level above the object, which is the nesting from session 1. Miss it and the grant looks correct and returns nothing.
Create the five functional roles. One per job in the design, named for the job. No object privileges go anywhere near these.
Grant access roles to functional roles. This is the wiring that makes the model a model. Read each line against the design grid as you type it, and if a line does not correspond to a row in the grid, stop, because you are inventing policy at the keyboard.
Grant functional roles to users. One each. If somebody needs two, that is a finding rather than a configuration: either the design is missing a job or the person is doing two, and both answers are worth having in writing.
Verify against the design, one role at a time. Run the statement below for each of the five functional roles and read the output next to the grid. You are checking two things: that every access role the design implies is present, and that nothing else is. The second check is the one that finds problems.
Capture that output to a file in the repository next to the design. Session 7 turns exactly this diff into a pipeline, and having last month's file is what makes the first run useful instead of merely correct.
Now make the mistake on purpose. Grant one functional role to another and look at what the analyst can suddenly read. Predict the answer out loud before you run the second statement.
The analyst now reads name, email, phone and support_notes in clear, across both of support's regions, and the support agent gained nothing. The privileges travelled up to the role above. Nothing in the design changed and nothing errored.
Practise the deletion procedure on the role you just made a mess with. Find the members, check the query history, then revoke rather than drop. The habit is the deliverable.
Put every statement above in version control before you run it. A role model that exists only as executed statements cannot be diffed, reviewed or rebuilt, and the first question after any incident is what changed and when. The file is also the input to session 7, so writing it now costs nothing later.
Before session 3
40 min- Build the two layers for one real table in your own estate, with the naming convention written down next to the design.
- List every role currently granted directly to a user that is not a functional role. Each one is a hole in the model, and the list is usually shorter than people fear.
- Find every functional-to-functional role grant you already have. Work out, for each, what the upper role gained, and check whether anybody intended it.
- Find every grant made to PUBLIC. For each, decide whether it should become an access role granted explicitly, or be revoked.
- Pick one orphan role and run the deletion procedure as far as the revoke. Diary the drop for two weeks out, and note what turned up during the wait, because something usually does.
- Count your roles against your distinct job descriptions. If roles outnumber jobs by more than about two to one, bring the list to session 3.
Three questions
You run grant role fr_support_agent to role fr_analyst_eu. Who can read name, email, phone and support_notes afterwards?
The statement reads as though it gives support something and it does the opposite. Privileges travel up to the role above, which is why functional roles are never granted to each other and why this shape in a grants dump is unambiguously a mistake.
A new table arrives that three of the five jobs need. What does the two-layer model require?
Adding an access role keeps the object privileges in the layer built to hold them. Granting the table straight to functional roles is the shortcut that rebuilds a single-layer model one table at a time, and it hides the grant from the layer a reviewer reads.
You have found a role with three members and no queries in 90 days. What is the safe next move?
Nobody should lose access at the moment of deletion, and query history is evidence rather than proof: a dormant role may serve an annual extract. The revoke window is what turns a cleanup into a reversible change.