← all posts
2026.09.09 · 6 min

Druxt (for Drupal) 1.3.x; the resource list is yours

DrupalDruxtPlanet Drupal

Druxt's Drupal module carries a list of twelve JSON:API resources it answers for. It's been the same twelve since 2021, hardcoded in a PHP array, and changing it has meant carrying a patch.

1.3.0, tagged today, makes that list yours. The twelve stay exactly as they are, so nothing a site exposes changes when it updates. What is new is being able to choose, and the first thing I would choose is the one my frontend has always had to guess at: the toolbar an administrator configured for a text format.

A CORS default ships in the same release. If you keep a proxy rule in front of your frontend so that authenticated calls succeed, this is the release where you delete it.

The module name suggests otherwise, but this is a Drupal-side change. Any client holding the right permission gets the same reads.

What it answers today

Anyone holding the access druxt resources permission can read these, whatever access control would otherwise have said:

The list, as it stands
block--block
configurable_language--configurable_language
entity_form_display--entity_form_display
entity_form_mode--entity_form_mode
entity_view_display--entity_view_display
entity_view_mode--entity_view_mode
field_config--field_config
field_storage_config--field_storage_config
jsonapi_resource_config--jsonapi_resource_config
menu--menu
menu_link_content--menu_link_content
view--view

Every one describes structure, and all of it is configuration, with one exception: menu_link_content is content, and we will come back to it. None of them describes how the site prefers things to look or behave, and with the list compiled in, nobody could add one that did.

Starting with rich text

A frontend offering an editing experience has no way to read the toolbar an administrator configured, so it ships whatever buttons its developer chose and hopes they match.

The reason it cannot read it is the interesting part. The only core permission that grants access to a text format's configuration is administer filters, which core itself flags restrict access. Hand it to an author and they can rewrite the text format, one of the shorter routes to an XSS. So the choice has been between a permission you cannot give them and a frontend guessing at a toolbar.

A checkbox is the third option, on a new settings form at Configuration > Web services > Druxt. It grants read where administer filters grants write, and it grants it to everyone holding access druxt resources instead of to one trusted account. List editor--editor and the frontend reads the toolbar and builds from it; change the toolbar in Drupal and the editor changes, with no rebuild: 10 buttons for basic_html, 16 for full_html.

The same toolbar, twice. Drupal draws the separators as draggable tiles because that screen is for editing the toolbar; the frontend draws them as dividers.

Which filters run is a second tick, on filter_format--filter_format. It tells a frontend whether a caption arrives in a data-caption attribute or in the markup. Fetch it once first: JSON:API hands over whole entities, so that is every format on the site and each one's filter settings, an open blob contrib modules fill with their own keys.

What comes back is Drupal's vocabulary, not CKEditor's. drupalInsertImage is Drupal's name for uploadImage, and an item your build has not loaded is dropped with a console warning rather than an error, so the editor comes up fine, quietly missing buttons the format says it has.

Reading the toolbar, server side
// Nuxt 2. fetch() runs on the server; druxt-site injects $druxt.
async fetch() {
  // A string: an object flattens to filter=[object Object] and 400s.
  const { data } = await this.$druxt.getCollection(
    'editor--editor',
    'filter[drupal_internal__format]=basic_html',
  )
  this.items = ((data[0] || {}).attributes || {}).settings.toolbar.items || []
}

All of which is me saying: expect a Druxt CKEditor module in the future. Mounting the editor is where the work actually is. CKEditor 5 uses static class blocks, which the bundler Nuxt 2 ships cannot parse, so today you load it from a script tag and match the version Drupal ships rather than the one npm defaults to. That is exactly the sort of thing a module should absorb so nobody else meets it.

Neither is ticked by default. Updating writes the same twelve into configuration and stops there, and nothing on the list is readable by anyone until access druxt resources is granted, which installing grants to nobody. Changing the list needs administer druxt, which ships restricted.

Then everything else

What else is a frontend inventing? All three of these are configuration entities, so they are already in that checkbox list, unticked:

  • Image styles. Effects carry the dimensions a srcset needs. The derivative URLs are solved already, by JSON:API Image Styles and Consumer Image Styles, after Lullabot named it in 2018
  • Editorial workflow. Content moderation's states and transitions would let an editing interface show where a document sits and which moves are legal, instead of a hardcoded draft-and-published pair
  • Bulk actions. action--action carries every action a site has configured, so a decoupled content list could offer exactly those

What it will not answer for

Druxt answers Drupal's entity access check with a yes for any listed route, and lifts JSON:API's limit on filtering by fields the requester could not otherwise see. A listed resource is readable by anyone holding access druxt resources, whatever the site would otherwise have said, and on a decoupled site that permission often goes to anonymous.

The list has one axis. It says what, never who: the same yes, site-wide, for everyone holding the permission. That suits configuration, which answers the same whoever is asking. Content varies per entity by published state, owner and field access, so one yes is never right for it. user--user would hand over every account, blocked ones included, and enough filtering to enumerate them.

menu_link_content is the exception, and it is content, grandfathered because it shipped in the hardcoded list. It is also the resource that gives a frontend the least, so I maintain JSON:API Menu Items to cover the gap.

Configuration entity types only, and no content ones offered at all. editor--editor and filter_format--filter_format are separate ticks, and the help text under the list says outright that a ticked resource is readable by anyone holding the permission.

CORS, and the proxy rule you can delete

One more thing, and it takes a step out of everyone's setup. On a site that has not configured CORS itself, Druxt turns it on and allows every header, and now sets allowedMethods too. That list is what the library underneath advertises methods from, and core ships it empty, so the browser refused the preflight and the request never left.

Anonymous GET was never affected, since a simple request is not preflighted. What changes is everything else: writes, and reads that set an Authorization header. If you keep a proxy rule or a middleware in front of your frontend so that authenticated calls succeed, this is the release where you delete it. Worth checking rather than assuming, because a refused preflight answers 204 with a clean log, and curl never reproduces it: only a browser enforces the response.

A site that configured cors.config itself is untouched, and credentials stay off, so cookies and sessions still do not travel cross-origin. An Authorization header does. The documentation site was rebuilt around tutorials last week, and the login flow is the one to start with if authenticated requests are what you have been putting off.

What are you working around?

Enough about what I would tick. What I want to know is what you are already working around. Anyone building against Drupal from outside it has written something that guesses at a value Drupal already holds. Mine was a toolbar. It is a checkbox now.

So what is yours, and what would you build if you could simply read it?

The Drupal module this ships in. If your projects lean on modules like it, sponsoring is what keeps them maintained.

Discussion

via GitHub Discussions