Please follow the recommended procedure to upgrade your agent version by following this note.
- 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. - 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, then run:In case of a regression introduced in Production after the upgrade, a rollback to your previous agent version is the fastest way to restore your admin panel. Nothing in this release writes to your database or to your Forest Admin project settings, so rolling the gem back fully restores the previous behavior.
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.
Two more checks are now enforced on the Related data section of a record:
- 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.
Only the collections a path reaches are checked. The collection the request is rooted on is not re-checked here —
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)).
Smart field getters are protected: a getter reading an undeclared column reloads the record once (at the cost of an extra query, logged once per process) and never returns a 500. This protection covers smart field getters only, not the model-level code listed above.
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 key linking it to its owner, such as:
ActiveModel::MissingAttributeError when preloaded. Do not name such an association in a dependencies: path, or add the foreign key 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:.
The validation above is lenient on purpose. The v2 agents reject the same declarations at boot — see If you plan to migrate to a v2 agent later.
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.
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.
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.
- Adopt
dependencies:andcountable: falseon your heaviest collections once the upgrade is stable.