2

Cross-posted on ApiPlatform's Github, now labeled as "bug": https://github.com/api-platform/api-platform/issues/2443


I'm setting up a skeleton for myself that has the basics included. Part of that is a User which can have Roles. A Role is not an Entity, it exists only as strings and they're stored as part of User records as a json column (Postgres).

The trouble I'm facing: Both POST and PATCH endpoints for creating and updating a User have the same Constraints. However, the PATCH endpoint fails to validate incoming data prior to trying to use it.

The POST /api/users & PATCH /api/users/:uuid endpoint are configured like so:

ApiPlatform\Metadata\Post:
    processor: App\User\PasswordHasher
    normalizationContext:
        groups:
            - user:item
    denormalizationContext:
        groups:
            - user:post
    validationContext:
        groups:
            - user:post
ApiPlatform\Metadata\Patch:
    processor: App\User\PasswordHasher
    normalizationContext:
        groups:
            - user:item
    denormalizationContext:
        groups:
            - user:patch
    validationContext:
        groups:
            - user:patch

The validation for this is configured like so:

roles:
    - Choice:
        #  callback: [ App\Role\Role, getRoles ]     # <-- Alternative, same result
        choices: [ 'ROLE_USER', 'ROLE_ADMIN' ]  
        multiple: true
        message: 'The role "{{ value }}" is not a valid role.'
        min: 0
        minMessage: 'You must select at least {{ limit }} choice(s).'
        groups:
            - user:post
            - user:patch

When debugging, I find that the class Symfony\Component\Validator\Constraint\Choice gets instantiated and the correct options are present:

Instantiated Choice Constraint object

However, the Symfony\Component\Validator\Constraints\ChoiceValidator::validate function is never called. I have set a breakpoint on the first line in that function, but it never gets there. (It is called when using the POST endpoint.)

Instead of using the validator, it continues as if the content is valid and tries to add an invalid "Role" value (INVALID) to the User object. That it even attempts this means it got past the validation stage.


Observations

  • The same code is being used for the POST /api/users endpoint, where using INVALID as the value fails with a 422 Unprocessable Content, as expected
  • Because the POST and PATCH endpoints have the same configuration for Resource (Entity) and the same Constraints, I expect the difference in handling to be because of the HTTP Method
    • POST calls the ChoiceValidator::validate function
    • PATCH does not call the ChoiceValidator::validate function

Additional code for context

Behat is the feature testing framework of choice for process and data validation. Below are the 2 tests, one for POST and the other for PATCH, that verify trying to save INVALID should fail.

POST /api/users - This one passes

    # Purpose: validate that an administrator cannot create a user with an invalid role
    Scenario Outline: An administrator cannot create a user with an invalid role
        Given there is no user with email "john.doe@example.com"
        And add "Accept" header equal to "<accept>"
        When a POST request is sent to "/api/users" with body:
        """
        {
            "email": "john.doe@example.com",
            "plainPassword": "SuperSecretPassword@123!",
            "roles": <roles>
        }
        """
        Then the response status code should be 422
        And the response should be in JSON
        And a user with email "john.doe@example.com" should not exist
        And the header "Content-Type" should contain "application/problem+json"
        And the header "Content-Type" should contain "charset=utf-8"
        And the JSON should be a valid item according to the schema "exception.schema.json"

        Examples:
            | accept              | roles                     |
            | application/json    | ["INVALID"]               |
            | application/ld+json | ["INVALID", "ROLE_ADMIN"] |

PATCH /api/users/:uuid - This one fails

    # Purpose: validate that an administrator cannot patch a user with an invalid role
    Scenario Outline: An administrator cannot patch a user with an invalid role
        Given there is no user with email "john.doe@example.com"
        And there is no user with uuid "1a2b3c4d-1a2b-1a2b-1a2b-1a2b3c4d5e6f"
        And there is a user with email "john.doe@example.com" and uuid "1a2b3c4d-1a2b-1a2b-1a2b-1a2b3c4d5e6f"
        And add "Content-Type" header equal to "application/merge-patch+json"
        When a PATCH request is sent to "/api/users/1a2b3c4d-1a2b-1a2b-1a2b-1a2b3c4d5e6f" with body:
        """
        {
            "roles": <roles>
        }
        """
        Then the response status code should be 422
        And the response should be in JSON
        And the stored value for roles of user with email "john.doe@example.com" should not contain roles:
            | <roles> |
        And the header "Content-Type" should contain "application/problem+json"
        And the header "Content-Type" should contain "charset=utf-8"
        And the JSON should be a valid item according to the schema "exception.schema.json"

        Examples:
            | roles                          |
            | ["INVALID"]                    |
            | ["INVALID", "ANOTHER_INVALID"] |

Output of failed step:

    Examples:
      | roles       |
      | ["INVALID"] |
        Failed step: Then the response status code should be 422
        Current response status code is 404, but 422 expected. (Behat\Mink\Exception\ExpectationException)
        │
        │  JSON body:
        │  {
        │      "type": "https:\/\/tools.ietf.org\/html\/rfc2616#section-10",
        │      "title": "An error occurred",
        │      "detail": "The role 'INVALID' does not exist.",
        │      "trace": [
        │          // THE STACK TRACE HERE

The output detail message comes from Role::addRole which checks if the provided Role string value INVALID exists in the array of known roles (['ROLE_USER', 'ROLE_ADMIN']).

rkeet
  • 3,406
  • 2
  • 23
  • 49

0 Answers0