Problem/Motivation

Any attempt to create a paragraph entity through jsonapi POST results in a 403 response and no paragraph created. This is because ParagraphAccessControlHandler::checkCreateAccess() only allows access when the request format is html.

This means jsonapi cannot be used to create new paragraph based pages. This is a shame, especially as jsonapi is moving in to core with 8.7.0.

This can be worked around by writing code, for example, implementing hook_paragraph_create_access() to allow creation of paragraphs. But it would be better if this was possible using just the paragraphs module with no custom code.

Proposed resolution

Add a new permission(s) to allow creation of paragraphs remotely.
Extend Drupal\paragraphs\ParagraphAccessControlHandler::checkCreateAccess() to check such permissions and allow operations.

Remaining tasks

Decide whether the permissions should be per bundle
Decide what other permissions we should allow for remote operations - eg update/delete/own/others.
Actually implement.

User interface changes

None

API changes

None

Data model changes

None

Release notes snippet

✄

Issue fork paragraphs-3047678

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

Eli-T created an issue. See original summary.

berdir’s picture

> Decide whether the permissions should be per bundle

The paragraph type module already provides per-bundle permissions.

eli-t’s picture

Status: Active » Closed (works as designed)

Aha! Paragraphs Type Permissions.

Thanks @berdir

berdir’s picture

Status: Closed (works as designed) » Active

Didn't mean to suggest closing this issue. The current implementation was done as a security fix because before, it required _no_ permission at all :)

The type permissions module is an option, but it can also be tedious to configure possibly dozens of permissions if you don't need that level of granularity.

berdir’s picture

This actually came up a few times already, so a good start might be a documentation page that explains the currently available options (custom create access hook or using the type permissions module).

logickal’s picture

Understanding that there should probably be more discussion around this issue, I do think that with jsonapi now in core, it's reasonable to think that Paragraphs should support creation via that method. Since we just ran into it, here is a quick patch that adds api_json to the valid types. Definitely needs tests as well.

logickal’s picture

Status: Active » Needs review
berdir’s picture

No, this check is specially there to exclude jsonapi.

You need to you your own hook with custom logic or use the type permission submodule

logickal’s picture

Status: Needs review » Active

Sorry @Berdir - I think I must have misunderstood #4, so my apologies. I then also later found #3012600: Unable to create Paragraph with format json which describes the same.

What is your feeling about adding a new permission that would get evaluated in checkCreateAccess() to allow JSON API creation globally?

miro_dietiker’s picture

IMHO it is a bad idea that json api does not introduce dedicated permissions or settings. Virtually exposing everything through the api can easily lead to vulnerabillities: You enable it just for a specific tiny task and everything js exposed.

It is not the job of Paragraphs to mitigate such a design limitation. IMHO core should fix the situation. That‘s why we decided for a defensive default behavior with the last security update.

If we add a custom perm, we later need to catch up with ugly updates in case core covers the requirement at some point.

This leads us exactly back to the root problem that core doesn‘t support composite entities (what we did in ERR). And core is exactly hitting the pains of it as well with moderated translatable custom blocks and more...

Let‘s see what berdir proposes as a feasible mitigation or next milestone focus.

GuyPaddock made their first commit to this issue’s fork.

guypaddock’s picture

Assigned: Unassigned » guypaddock

I'm working on an alternate approach that provides a way to "sidepost" paragraph entities through a parent's JSON:API resource.

guypaddock’s picture

Title: Allow creation of paragraphs through jsonapi » Allow creation of paragraphs through JSON:API
Status: Active » Needs review

The approach in MR 24 implements a way to create or update paragraph entities through JSON:API requests against the parent resource.

Concept and Approach

As mentioned in my earlier comment, this is a concept known as "sideposting" -- the idea that you can effect change on multiple related entities under the same top-level request. Ideally, JSON:API would have finalized their own sideposting specification but it's been under debate since 2017 and they still haven't come to consensus.

My intent is to introduce a mechanism for persisting entities with nested paragraphs using standard JSON:API clients. So, that ruled out using Subrequests because that's an RPC-style approach that is radically different from a standard JSON:API request. With Subrequests, the client not only has to construct the requests a special way, but then has to check on the status of each sub-request afterwards.

Requirements

This approach requires the patch to Entity Reference Revisions from #3267304: Infer "target_revision_id" to be default revision when only a "target_id" is provided to function. Without the patch, nested paragraphs would be created or updated but would not get linked-up to their parent entity because ERR currently requires the client to provide revision IDs.

How this Works

To accomplish the goal of this change, I added an event subscriber that provides sideposting behavior. The new behavior only kicks in on JSON:API requests that opt-in for it. The client opts-in via a new "meta.persistence_behavior" key on the included entity (the value must be "create_or_update"). The client also has to generate and assign a valid UUID for every sideposted paragraph entity.

For example, an outgoing POST request to /jsonapi/node/sample could look like:

{
  "data": {
    "type": "node--sample",
    "attributes": {},
    "relationships": {
      "field_paragraph": {
        "data": [
          {
            "id": "62ff56ab-56bd-425a-908c-7556638add54",
            "type": "paragraph--sample"
          }
        ]
      }
    }
  },
  "included": [
    {
      "id": "62ff56ab-56bd-425a-908c-7556638add54",
      "type": "paragraph--sample",
      "attributes": {
        "field_favorite_food": "Macaroni and Cheese",
      },
      "meta": {
        "persistence_behavior": "create_or_update"
      }
    }
  ]
}

In the example above, the UUID "62ff56ab-56bd-425a-908c-7556638add54" was generated by the client.

Updates via PATCH are also supported, allowing the nested paragraph to be updated at the same time as the parent. For example, an outgoing PATCH request to /jsonapi/node/sample/6d521732-3ef3-4aa1-a779-96163ecd3c6d could look like:

{
  "data": {
    "id": "6d521732-3ef3-4aa1-a779-96163ecd3c6d",
    "type": "node--sample",
    "attributes": {},
    "relationships": {
      "field_paragraph": {
        "data": [
          {
            "id": "62ff56ab-56bd-425a-908c-7556638add54",
            "type": "paragraph--sample"
          }
        ]
      }
    }
  },
  "included": [
    {
      "id": "62ff56ab-56bd-425a-908c-7556638add54",
      "type": "paragraph--sample",
      "attributes": {
        "field_favorite_food": "Hot Dogs",
      },
      "meta": {
        "persistence_behavior": "create_or_update"
      }
    }
  ]
}

If a paragraph identified by UUID 62ff56ab-56bd-425a-908c-7556638add54 does not exist, it will be created with a POST Symfony sub-request. If it does exist, it will be updated in-place via a PATCH sub-request.

On the server side, the event subscriber does the following:

  1. Starts preprocessing data before JSON:API does.
  2. Searches through the included relationships of the top-level resource object for any paragraph entities that the client has signaled will need sideposting. If there are none, the event subscriber does no further processing on its own (JSON:API in Core handles the rest of the request).
  3. Starts a new database transaction to wrap the sub-requests.
  4. For each included paragraph entity that needs sideposting:
    1. Attempts to load the existing paragraph.
    2. If the paragraph already exists in the database:
      1. Confirms that the paragraph either has no parent set or that its parent matches the top-level object that's being updated.
      2. Issues a POST sub-request to create the paragraph. The sub-request carries with it all the included objects from the top-level document, in case we need to handle deeper-nested relationships.
    3. If the paragraph already exists in the database: Issues a PATCH sub-request to update the paragraph. The sub-request carries with it all the included objects from the top-level document, in case we need to handle deeper-nested relationships.
  5. If any validation or persistence errors get raised by sub-requests: the transaction gets rolled back and we bubble-up the error. Otherwise, we proceed with the top-level request and JSON:API in Core handles the rest.

Benefits

This approach has the following benefits over other approaches:

  1. Sub-requests can be accomplished with minimal changes to standard JSON:API interactions (unlike the Subrequests module).
  2. Paragraphs can be created and updated in the same request as their parent entities, reducing the number of round-trips for frontend clients and making the request seem more atomic (unlike standard JSON:API interactions where you have to create all dependencies first in separate requests and then create the top-level object).
  3. Paragraph entities can only be created through their parent resource, avoiding the complication of introducing additional permissions checks or access control logic (unlike what was being discussed earlier in this issue).
  4. Since the included array gets passed through all the sub-requests, this approach should traverse paragraphs depth-first. If paragraphs contain other paragraphs fields, the deepest entities of the object graph should get created/updated first, followed by the higher levels, and so-on.

Limitations

This approach does have some flaws/limitations:

  1. For performance and simplicity reasons, the new behavior will only be triggered by paragraphs referenced by the top-level resource. If you are trying to persist a larger object graph in which paragraphs are 2+ levels removed from the top-level resource (e.g., node -> node -> paragraph, node -> taxonomy term -> paragraph), this approach won't be an improvement over what JSON:API in core does. To address this use case, this approach would need to be expanded to support more types of resources than just Paragraphs, but that's beyond the scope of the Paragraphs module.
  2. Though a DB transaction is used for all the sub-requests, this approach is not transactional if all the sub-requests succeed but the top-level resource fails to validate or save. In such a scenario, the paragraphs created by the sub-requests will remain even after the error gets sent back to the client. I had considered starting a DB transaction at the beginning of the request and then rolling it back before sending back a response if the response was unsuccessful, but the scope of such a DB transaction would be very large and I didn't feel comfortable assuming that this behavior is what a site admin would accept (e.g., watchdog logs and entities created/updated by external services might end up included in the transaction, so a rollback could be very detrimental). I would recommend that if a site admin wants this kind of "all or nothing" behavior, they should implement it themselves through a separate custom module on their site instead.

Security Considerations

From the discussion earlier in this thread, I recognized a need to restrict what users can create paragraphs. As is the case before this approach, users do NOT have the ability to create paragraphs directly through JSON:API as allowing this without additional permissions would have opened-up a security hole.

Borrowing from TemporaryJsonapiFileFieldUploader in Core, we check the user's field access. The JSON:API client is not allowed to perform a sidepost if any of the following are true:

  • If the field through which they are sideposting is not a Paragraphs reference field.
  • If the user does not have permission to create the top-level entity.
  • If the user does not have permission to update the top-level entity.
  • If the user does not have permission to update the specified Paragraphs field of the top-level entity.

Next Steps

If this approach meets the approval of the maintainers, assign back to me as NW and then the next steps are:

  1. Add functional tests.
  2. See if we can include a meta.persistence_status value in responses to signal to the client if a sidepost resulted in a 201 or 200, in case they need that metadata to know whether a particular paragraph was created or updated.
JBHUTT09’s picture

I've applied the latest version of this patch and the patch from #3267304 (listed as a requirement), but when I send a POST request I get a response saying that the paragraph item cannot be found.

This is my request uri:

/jsonapi/node/page?include=field_phone_numbers&jsonapi_include=1

This is my request body:

{
  "data": {
    "type": "node--page",
    "attributes": {
      "title": "Paragraphs Test",
      "body": {
        "value": "Custom value",
        "format": "plain_text"
      },
      "field_external_guid": {
    	"value": "123456"
      }
    },
    "relationships": {
    	"field_phone_numbers": {
    		"data": [
    			{
    				"id": "0637e903-7676-4034-b04b-4f94ff861516",
    				"type": "paragraph--phone_number"
    			}
    		]
    	}
    }
  },
  "included": [
	{
		"id": "0637e903-7676-4034-b04b-4f94ff861516",
		"type": "paragraph--phone_number",
		"attributes": {
			"field_formatted": "(555) 555-5555",
			"field_raw": "5555555555"
		},
		"meta": {
			"persistence_behavior": "create_or_publish"
		}
	}
  ]
}

Has something changed since this was last worked on or am I doing something wrong?

guypaddock’s picture

I believe the persistence_behavior needs to be create_or_update, not create_or_publish.

JBHUTT09’s picture

Oh my god. That was it. Sorry to bother you for something so dumb and thank you so much.

guypaddock’s picture

Sorry for the delay, JBHUTT09 -- no worries at all!

mxr576’s picture

beauby on Dec 11, 2022
Closing this as I am not actively working on it, and a lot has changed since I stopped.

Source: https://github.com/json-api/json-api/pull/1197#issuecomment-1345658588

So the Sideposting spec draft was closed on the JSONAPI side :(

What I am aware and might be related is https://jsonapi.org/ext/atomic specification that https://www.drupal.org/project/jsonapi_operations tried to implement once, but it is also not production ready.

mxh’s picture

Needs review over two years, maybe this is a "won't fix"?