Skip to main content
Fine-grained access control lives in your Malloy models and is enforced on every query. While resource hierarchy controls access at the environment and package level (“can you see this package?”), fine-grained ACLs work at the source level, in three layers:
  1. Row scope — which rows do they see? Filter with a where: clause over a secure given.
  2. Source access — can this caller query the source at all? Gate it with #(authorize).
  3. Column scope — which fields are exposed? Restrict them with accept:/except:, and gate sensitive columns with a separate #(authorize) source.
Row scope and source access decide access from secure givens. Givens are a Malloy language construct — named values a model declares once and receives at query time, referenced with $. A secure given is one Credible fills server-side from the caller’s verified identity — their email — so a caller cannot forge it. Column scope builds on the same #(authorize) gate. If you need identity resolved from something other than email, reach out.
This is separate from discovery curation (explores / queryableSources in publisher.json), which controls which sources are listed and queryable by name — not who may query them.

Row scope: secure givens

Scope rows with a where: clause over a secure given — a given: Credible populates from the caller’s verified identity. Givens are declared at the top of the model (not inside a source) and referenced with $. Mark a custom given #(secure) and declare it set-valued (string[]), then filter with a membership test (in) — secure givens are set-valued by design (a scalar secure given isn’t enforced).
The given’s values aren’t in the model — they’re a lookup table you manage on the Access Control page. Each row grants a user (by email), a group, or everyone (a default) a list of values — and the values are the literal data values your where: compares against (here, tenant_id values). At query time Credible takes the caller’s verified email, resolves their groups, and merges every applicable row into one set: a group grant is a floor shared by all its members, and individual users can be topped up with extra values. A caller with no applicable row resolves to an empty set, which matches nothing — access fails closed. For the example above: grant the support group ["acme"] and alice@yourco.com ["globex"], and Alice — a member of support — resolves $ALLOWED_TENANTS to ["acme", "globex"] and sees both tenants’ rows, while her teammates see only acme’s. Referencing a custom secure given in a published model is also how the platform learns it exists — the attribute appears on the Access Control page once a model gates on it — so declaring #(secure) ALLOWED_TENANTS is only half the setup; assigning values is the other half. $GROUPS is built-in — declare GROUPS :: string[] in the model (no #(secure) needed), and Credible fills it with the names of the groups the caller belongs to in your organization (the same groups you manage in Users & Groups), with no admin-portal assignment. The values are literally the group names, matched against your data exactly, including case — a group named West won’t match a west value. So filtering on $GROUPS means naming groups after your data values: to scope rows by region, create a group per region (west, east, …), add each user to their regions, and filter with a membership test:

Source access: #(authorize)

Gate whether a caller can query a source at all with #(authorize), a Malloy boolean expression over the model’s givens, placed above the source:. A source with no #(authorize) is unrestricted; stack multiple and they combine as OR (any true grants).
The gate can read any given, not just $GROUPS — including a custom secure given whose values you assign per user or group on the Access Control page. That lets you grant source access to specific individuals without hardcoding emails in the model:
Assign dana@yourco.com a user-scope value of ["acme"] and she passes the gate; change or remove the assignment and her access follows — no republish needed.
An access gate is only as trustworthy as the given it reads.
  • Secure — a #(secure) set-valued given (string[]), or the built-in $GROUPS. Credible resolves these server-side and strips any caller-supplied copy. Filter and gate with in.
  • Bypassable — a plain or scalar given a caller can set. The caller can send the value and pass the gate. Credible flags a non-blocking advisory when an #(authorize) gate reads a non-secure given.
  • A where: over an ordinary (non-secure) given is fine for parameterization — it just isn’t an access boundary.

Column scope: restricting fields

Control which fields a source exposes with Malloy’s accept: (an allowlist) or except: (a denylist), inside a source extend. These are static — they can’t read a given, so one source can’t show a column to some callers and hide it from others. (accept: and except: can’t be combined.) Say the orders table has five columns: order_id, status, amount, customer_email, and credit_card_number. Either style hides the sensitive ones:
Both expose exactly order_id, status, and amount — querying credit_card_number against either is a compile error, because the field doesn’t exist on the source. To expose a column to some callers only, split into two sources and gate the full one with #(authorize):
Callers in the billing group query orders_billing and see every column, including credit_card_number. Everyone else queries orders, where the sensitive columns don’t exist. #(authorize) gates querying, not discovery — the gated source still appears in listings (name, fields, docs) to callers who can’t query it. To hide it from listings while keeping it queryable for authorized callers, curate it out of discovery with queryableSources: "all" — see discovery curation.
Malloy also has experimental public / private / internal field modifiers, which control whether a field can be used in queries and in source extensions. Like accept:/except:, they are static — not per-caller access control. Use the source-split pattern above for identity-based column access.

Have custom access control requirements? Contact us to discuss your use case.

Next Steps

Embedded Analytics

Build data applications with user-specific access

Users & Groups

Configure environment and package-level access