Writing rules¶
A policy is a list of rules over tables and fields, by name and label. Bind it to the app once, which looks up every name and reports every problem at once, then give the bound policy to the client.
from pyquickbase import F, Policy, Quickbase, allow, deny, fields, limit, tables
policy = Policy(
classes={
"pay": ["Tasks.Amount"],
"internal": [("Tasks", "Code"), fields.labeled_like("Old*")],
},
rules=[
allow.read(tables.all(), max_paging_depth=50),
allow.insert("Tasks", fields.all()),
allow.update("Tasks", ["Status", "Due Date", "Notes"]),
allow.delete("Tasks", max_records_per_delete=3),
limit.rows("Tasks", F("Project") == "Roof"),
deny.all(fields.classed("pay", "internal")),
],
)
with Quickbase(realm="acme", token="your_user_token") as setup:
bound = policy.bind(setup.app("bq1234567"))
with Quickbase(realm="acme", token="your_user_token", policy=bound) as qb:
tasks = qb.app("bq1234567").table("Tasks")
tasks.update([{"Record ID#": 1, "Status": "Done"}]) # Allowed
# tasks.update([{"Record ID#": 1, "Amount": 90}]) # Raises PolicyRefusedError
With the async client, it's await policy.bind(app). Bind with a client that has no policy, so it
can see every table and field.
Anything no rule allows is refused, and a deny always wins. Each rule names its tables first:
a table's name or ID, a list of them, tables.all() for every table in the app and any the client
creates, or tables.created() for only those the client creates. Most then name fields: labels or
IDs, fields.all(), fields.new() for fields added after binding, fields.classed(...), or
fields.labeled_like(...), which matches labels such as _*, ignoring case.
| Rule | What it allows |
|---|---|
allow.read(tables, fields, ...) |
Reading the fields, and filtering and sorting by them, unless it gives its own filter= or sort=. It also takes max_paging_depth, max_records_scanned, and user_emails |
allow.filter(...), allow.sort(...) |
Filtering or sorting by fields without reading them |
allow.insert(...), allow.update(...) |
Setting the fields in new or existing records. An upsert needs both |
allow.delete(tables, max_records_per_delete=None) |
Deleting records. With max_records_per_delete, the client counts what matches first |
allow.download(...), allow.delete_file(...) |
Downloading files, or deleting their versions |
allow.report(table, reports) |
Running these reports, by name or ID, or every report with None |
allow.schema(tables, *verbs, ...) |
Changing the schema, as below |
allow.create_tables() |
Creating tables in the bound app |
deny.all(selector), deny.read(...), deny.update(...), and the rest |
Refusing what they name, whatever allows it |
limit.rows(tables, where) |
Reaching only the records that match, as below |
Things to know:
- Writing a field doesn't need read access to it, so a client can set a field it can't see. Such a field can still be named, but its values aren't returned.
Record ID#is readable wherever anything is, because the SDK uses it to page through records.max_paging_depthlimits each query, not the client. A query needs alimitof at most this, and it's refused rather than cut short. Where several rules read a table, the highest applies. It defaults to 100, andmax_records_scannedto 10,000. A summary that only counts reads no records, somax_records_scanneddoesn't limit it.- A field you may filter or sort by reveals something about its values, even if it isn't returned, because a model can see which records match and in which order.
- A report's own filter and sort can use any field, and someone can change them in the Quickbase interface, so list only reports you've checked. A summary report's columns can't be matched to fields, so allowing one shows all of them.
- Some things are refused unless the policy is
Policy.read_only(): running a formula, which can read any table, an app's details and roles, and any request it doesn't recognize. Use a separate client for those. - A delete needs
filteron the fields its filter uses, because the number deleted shows how many records matched. insertalone can't be limited to creating records on a table with a custom key field. If someone has changed a table's key field in the Quickbase UI, a row with an existing value in that field changes that record.table.insert()refuses those tables, so useupsert(), which needs bothinsertandupdate.qb.request()can't look up a table's key field, so any write through it without a merge field needs both.