Control who sees what
Access in MatterData is declared, not programmed. You write the rule on the page, and the engine puts it into the SQL that reads the rows. There is no place in an app where you can forget the check, because there is no place where you write it.
This guide covers three levels: who can open a page, which rows they see, and which columns.
Restrict a page to a role
Section titled “Restrict a page to a role”Every page declares the role that can open it. A page with no rule admits nobody.
page members "Members" { route: /members access: board
report of members "All members" { columns: name, email, joined_on }}Two reserved role names can be written in a rule. public admits anyone, including a visitor
with no account. authenticated admits anybody who holds a role in this application.
access: publicauthenticated is not “has a session”. An organization can hold several applications, and a
login with no role in yours is anonymous in yours. Read
An organization, its applications and one login.
Three more names are reserved and are refused in a rule and in a declaration: owner,
anonymous, and tenant_admin. See Reserved names.
A request from a role that is not admitted gets a 404 and never a 403. A 403 confirms that the page exists.
Show a person only their own rows
Section titled “Show a person only their own rows”Add where to the rule. The condition is about a row of the page’s table.
page my_notes "My notes" { route: /notes access { office author where owner = :user.id }
report of notes "Notes" { columns: title, released, updated_at }}The office sees every note. An author sees the notes she owns. Both use one declaration and one URL.
:user.id is the signed-in user. The engine resolves it once per request, before it renders
anything.
The condition becomes part of the query. It is not a filter applied to rows that were already read, so a row that the rule excludes never leaves the database.
Reach through a reference
Section titled “Reach through a reference”A condition can walk a reference, up to four steps.
access { teacher where course.teacher = :user.teacher_id}Narrow one region further
Section titled “Narrow one region further”A region can carry its own rule. The region’s rule narrows the page’s rule and can never widen it.
page overview "Overview" { route: /overview access: authenticated
report of notes "Public notes" { columns: title where: released }
form of notes "Add a note" { access: office fields: title, body actions: save }}An author opens the page and sees the report. The form is absent for her, and not refused. A body that names the form anyway stores nothing.
Hide a column from a role
Section titled “Hide a column from a role”Mark the column sensitive. After that, only a role whose rule grants the column can read it.
table grades "Grades" { display: student
student ref(students) required "Student" points int sensitive required "Points" remark text sensitive "Remark"}page grade_sheet "Grades" { route: /grades access { head_teacher { grants: grades.points, grades.remark } teacher where course.teacher = :user.teacher_id { grants: grades.points } }
report of grades "Grades" { columns: student, points, remark }}The head teacher reads both columns. The teacher reads the points and not the remark. For the teacher the remark is absent: no cell, no heading, and no name that a request can use.
The same grant decides writing. A form renders a control, and a sheet an editable cell, for a
granted column only. A request that names an ungranted column is ignored and logged.
A column derived from a sensitive column must be marked sensitive too. The compiler
refuses one that is not, because an aggregate over one row is that row’s value.
Rules that apply everywhere
Section titled “Rules that apply everywhere”These hold for every app, and you do not write them.
- A row that the reader cannot see returns the same 404 as a row that does not exist.
- The row rule applies to writes. An invisible row cannot be updated or deleted.
- The row rule applies to exports. A CSV file contains the rows the reader can see.
- Every form that changes data carries a CSRF token.
- SQL is always parameterized. A column name that arrives in a request goes through an allowlist.
Make sure that the rules are right
Section titled “Make sure that the rules are right”Access is asserted by a generated matrix over every route and every role, including anonymous. A new route with no rule fails that test. When you change a rule, the matrix is where you see what changed.
For what the engine enforces around your rules, and the three places it needs your discipline, read Security model.