forest_liana gem. If your project runs the v2 Ruby agent (forest_admin_rails / forest_admin_agent gems), it does not apply to you.- Role permissions are enforced on related data: fields, filters, sorts and searches that reach a collection a role cannot read are refused or redacted, the same way they are on Node.js agents.
- Lists select only the columns the page actually displays, instead of
SELECT *. - Smart fields can declare the columns and relations they need with a new
dependencies:option, removing N+1 queries on lists, related lists and CSV exports. - A collection can disable its record count with
countable: false, without overriding a controller. belongs_toandhas_onerelations to another database are preloaded once per page on lists and CSV exports (and on related lists under Rails 6.1), instead of once per row.- The agent announces its capabilities to the Forest Admin interface, which enables the projection features above and lets the interface adapt to what your agent supports.
Upgrading to v9.22
If you are upgrading from an older major version, please make sure you have also read the previous upgrade notes (v9, v8, v7, …). To upgrade, update the version in your Gemfile to at least 9.22.2:Deployment order with the Forest Admin interface
The interface reads the capabilities your agent announces and enables features one by one. You can upgrade the agent before or after the interface is updated on our side:- Agent upgraded first: everything described in this note applies immediately on the agent side (permission refusals, redaction, column selection, count deactivation). The interface simply does not use the capabilities yet.
- Interface updated first: your current agent does not serve the capabilities route, so the interface keeps its current behavior until you upgrade.
Before you upgrade
Work through this checklist in your development environment. Each item names what changes, how to detect whether you are affected, and what to do.1. Review the read permissions of your roles
Until now, a role could see the data of a related collection through a relation, a filter, a sort or a search even without the read permission on that collection. The agent now resolves every path to the collection it reaches and checks the role’s permissions on it.- Listing or counting related records requires the browse permission on the related collection itself, and exporting a related list to CSV requires
exporton it. A role that could open a parent record and see or export a related list without those permissions will now get a 403 there. - Associating or updating related records requires edit; dissociating or replacing records that get destroyed requires delete.
browse (list), read (record) and export (CSV) already gate it — so a role that may browse a collection without reading it keeps seeing its list.Scopes injected by the agent are never checked: only the filter tree sent by the caller is. Smart field search: lambdas are not checked either, by design, since their reach cannot be described.2. Temporary opt-out: skip_relation_read_permissions
If you need to upgrade before your roles are reviewed, you can keep the previous behavior for relation paths:
true, every exposed collection reached through a relation path is treated as readable: the redaction of related fields and the refusal of filters, sorts and searches described in the previous section are disabled. The agent logs it at startup and announces it to the interface, so that the interface does not rely on server-side pruning.
It does not disable:
- Collection-level permissions (
browse,read,edit,delete,add,export), including the new browse check on related lists. - The refusal of paths reaching a collection that is not exposed to Forest Admin.
- Scopes, segments, and smart action permissions.
3. Check the read_only: and is_searchable: options of your collections
The options collection :X, read_only: true and collection :X, is_searchable: ... in lib/forest_liana/collections/*.rb were silently ignored on collections backed by an Active Record model. They are now honored.
How to detect it:
read_only: true will lose its create, edit and delete buttons in the interface as soon as you upgrade. Remove the option if it was left there by mistake.
4. Check model code that reads columns the page does not display
Lists now select only the columns the request asks for, plus the primary key, the foreign keys of the relations in the request, the STI type column, and the columns of your model’s default ordering. Anything the query loads on its own is covered too: a relation joined by a Forest scope is selected whole, and a relation an association scope or adefault_scope preloads keeps the key the preload reads off the row. Any Ruby code that runs on these records and reads another column raises ActiveModel::MissingAttributeError, which answers 500.
Typical places to look at, on your models:
after_findandafter_initializecallbacksto_sornameoverrides used as record labelsdefault_scopeblocks and decorators reading attributes- Smart actions and route overrides that reuse the records of a list
dependencies: (see below), or make the code tolerant to a missing attribute (object.has_attribute?(:column)).
5. Check your search configuration
Two search behaviors change:- A search whose term matches no real column now returns no records instead of the whole table. You are affected if a collection’s
search_fieldsnames only smart fields without asearch:lambda, or only dotted relation entries used on a plain (non-extended) search. Previously the search silently returned every record. - A plain search (the default, without “extended search”) no longer joins the related tables to count the results: the count matches what the list actually searches. Collections that keep a smart field
search:lambda keep their joins.
search_fields, or give the smart field a search: lambda (see the reference).
6. Check the associations you will preload
The newdependencies: option (see New options) preloads relations instead of loading them once per row. One shape of association is incompatible with preloading: an association whose own scope narrows its select and drops the column the preload matches records on. That column is in the target table: the foreign key for a has_many or has_one, the target’s primary key (id, or the primary_key: option) for a belongs_to. For example:
ActiveModel::MissingAttributeError when preloaded. Do not name such an association in a dependencies: path, or add that column to its select.
7. Check your route and controller overrides
If you override Forest routes or controllers:- Permission refusals raise
ForestLiana::Ability::Exceptions::UnauthorizedFieldsError,UnauthorizedQueryFieldErrorandUnexposedQueryCollectionError, rendered as 403. Overrides that rescue only the previous error classes should be extended. - The record view, bulk delete and the related-data actions now render every other
ForestLiana::Errors::ExpectedErrorthemselves, with the error’s own status — as the list and the count already did before this release. Arescue_from ForestLiana::Errors::HTTP422Error(or similar) you added in an override no longer fires for an error raised inside those three — rescue it inside your override, or rely on the status the agent already returns. The three permission classes above are the exception: they are re-raised on purpose, so that theirnameanddatareach the response. - A filter, sort or projection whose prefix names no relation now answers 422
Relation not found: '<Collection>.<relation>'instead of resolving to the root collection. - A malformed filter (a raw value instead of a condition tree) answers 422
Filters cannot be a raw valueinstead of a 500. - Count overrides that call
deactivate_count_responsekeep working. For a whole collection,countable: falseis now simpler (see below).
New options you can adopt
None of these are required. They are the features this release adds, and they can be adopted one collection at a time — with one caveat ondependencies:, which changes how the requests displaying the field are queried.
Disable the count of a collection
{"meta":{"count":"deactivated"}} without running any query. The interface shows the list without a total. On a related list, the option of the related collection applies.
Declare what a smart field needs with dependencies:
A smart field getter runs once per record. Declaring what it reads lets the agent select the right columns and preload the right relations for the whole page:
dependencies: accepts a string, a symbol, or an array of them.
What you get:
- A list, related list or CSV export that displays the field runs one query per relation of the path instead of one per row.
- The columns named are selected with the list, so the getter reads them without an extra query.
- The field keeps working exactly as before. A request that displays a smart field with no
dependencies:keeps the previousSELECT *behavior for that request, and a getter reaching a relation keeps its one-query-per-row cost. - You can therefore declare your smart fields one at a time, starting with the ones on your largest lists.
- An entry that does not resolve to a real column or relation, or that crosses a polymorphic relation, is ignored with a warning, and the field behaves as if it declared nothing.
- On Rails 6.1, a path crossing an association with an instance-dependent scope cannot be preloaded: it is logged once and loaded per record as before.
dependencies:.
Projection on records and related data
The interface now sends the agent the exact fields it displays, on lists, records and related data. Records (show) used to load every column and every to-one relation; they now load only what is displayed. Nothing is required on your side: this is enabled by the capabilities your agent announces.
Adapt your code to get the full benefit
forest_liana gem, from 9.22. It does not apply to the v2 Ruby agent (forest_admin_rails / forest_admin_agent gems), which declares the same options with its own syntax.Declare dependencies: on your smart fields — Forest customization
dependencies: makes every request that displays it fall back to SELECT *. If its getter reaches a relation, it also runs one query per row. Declare the columns and relation paths the getter reads:read the "..." column without declaring it warnings in your logs: each one names a column that is missing from the declaration.Replace lookup smart relationships with real associations — Forest customization and application code
SELECT * for the whole page, and its block runs once per row. When the relation can be described by a foreign key, declare a real belongs_to or has_one on the model instead. Every list that displays it then gets column selection back, and the agent loads the relation for the whole page instead of one query per row.This step touches both kinds of code: the smart relationship is removed from your Forest customization, and the association is added to your application model.The association is already declared on the model
If your model already declares an association with the same name as the smart relationship, you have nothing to add: only remove the smart relationship from your Forest customization.Keeping both does not break the upgrade: requests that display the relation behave exactly as they did before 9.22. But the Forest declaration is not merely redundant, it takes precedence over the association:- Its block serves the value, still once per row. The association the agent joins for the list is loaded, then ignored.
- Every request that displays the relation keeps the
SELECT *behavior, as for any smart relationship. - The relation is listed twice in
.forestadmin-schema.json. - For a smart
has_many, the related list is served by the route you declared for it (/forest/<Collection>/:id/<relation>), not by the agent.
primary_key: and foreign_key: options shown above. For a smart has_many, also remove the route and the controller action that served it.Keep the matching key in association scopes — application code
select must keep the column the preload matches records on: the foreign key for a has_many or has_one, the target’s primary key (id, or the primary_key: option) for a belongs_to. Otherwise, as soon as a dependencies: path names it, the preload raises ActiveModel::MissingAttributeError and the request answers 500 (see check 6):Disable the count of your largest collections — Forest customization
deactivate_count_response, replace the override with the collection option:If you plan to migrate to a v2 agent later
The features of this release exist on the v2 agents (@forestadmin/agent for Node.js, forest_admin_agent for Ruby) with the same option names and the same 403 messages, so what you configure here carries over. Two behaviors are more lenient on this v1 release than on v2. If a migration to v2 is on your roadmap, writing your customizations the strict way now saves you a boot failure later.
Invalid or empty dependencies: fail the agent boot on v2
On this release, a dependencies: entry that does not resolve (unknown column, unknown relation, path crossing a polymorphic relation) is ignored with a warning, and dependencies: [] is accepted. On v2, both are rejected when the agent starts:
- an unknown column or relation, or a path crossing a polymorphic relation, raises a schema error and stops the agent;
- an empty list raises
Computed field '<Collection>.<field>' must have at least one dependency.
Invalid dependency ... warnings of this release as errors and fix them now, and give every computed field at least one real dependency instead of [].
A smart field search: lambda is not exempted from permission checks on v2
On this release, a smart field search: lambda runs on every search without any permission check, because the fields it reaches cannot be described. On v2, a search handler whose reach is not declared is refused on extended search with a 403 (You cannot run an extended search on the '<Collection>' collection: the fields it reaches cannot be determined, so they cannot be checked against your permissions. on Node.js; on Ruby, unless the handler declares the fields it reaches). Plain search keeps working on both.
What to do: nothing on this release. When migrating, expect to declare the fields your search handlers reach, or to restrict them to plain search.
Behavior changes at a glance
Logs to watch after deploying
Search your agent logs forForest 🌳🌳🌳 after the first requests in each environment:
ForestLiana.skip_relation_read_permissions is true: ...— the opt-out is active. Plan its removal.Invalid dependency '...' declared on smart field '...' of the '...' collection: it does not resolve to a real column, or crosses a polymorphic relation. Ignored ...— fix the declaration.Invalid dependencies declared on field "...": expected a String, Symbol, or Array of them. Ignored ...— fix the declaration.Field "..." of the "..." collection read the "..." column without declaring it in dependencies: — reloaded the record to serve it, at the cost of an extra query. Add it to the field's dependencies: to avoid this.— add the column todependencies:.Cannot retrieve the "..." value of the "..." collection because of an internal error in the getter implementation: ...— the getter could not be served (it raised again after a reload, or read a column of a relation that a reload cannot restore); its value is served asnil.The "..." dependency of the "..." collection cannot be preloaded ("..." has an instance-dependent scope, which Rails 6.1.x cannot preload) — the relation is loaded once per record instead ...— expected on Rails 6.1; upgrade Rails to benefit from the preload.The "..." relation of the "..." collection lives in another database and cannot be preloaded (its "..." key is not in the projected select) — ...— the cross-database relation shows as empty. Report it to Forest support with the collection and relation names.The "..." relation of the "..." collection cannot be preloaded (its "..." key is not in the query's select) — ...— a segment or scope narrows theselectof the collection and drops the key the relation reads. Add that key to theselect.
Rollout recommendation
- Upgrade in development. Restart the agent and read the startup logs.
- Go through the Before you upgrade checklist with each role your teams use.
- Decide whether you set
skip_relation_read_permissionsfor the transition, and record when you will remove it. - Deploy to staging, then production, following Push your new version to production.
- Watch the logs above and the 403/422/500/503 rate of your agent for a few days.
- Once the upgrade is stable, adapt your code on your heaviest collections.