noirbizarre / flask-restplus

Fully featured framework for fast, easy and documented API development with Flask
http://flask-restplus.readthedocs.org
Other
2.73k stars 507 forks source link

@api.expect() doesn't block extraneous fields #614

Closed abadyan-vonage closed 5 years ago

abadyan-vonage commented 5 years ago

Maybe I misunderstand, but it seems to me that @api.expect should filter fields not in the model or throw an error: https://flask-restplus.readthedocs.io/en/stable/swagger.html#the-api-expect-decorator

The current behavior is that it doesn't do that. If it shouldn't block those fields by design, what other way do I have to not let extraneous fields into my business logic?

Currently I'm manually filtering request.json according to the model in expect().

j5awry commented 5 years ago

I'll look a bit into the code (as i'm looking at API doc things now) but some quick thoughts:

1) json schema generally allows for extra parameters

The additionalProperties keyword is used to control the handling of extra stuff, that is, properties whose names are not listed in the properties keyword. By default any additional properties are allowed. json schema object information

2) be sure to set validate=True for validation. That being said, 1 likely supersede's 2.

I'll look more into the code and see what's up. It may be that this requires an enhancement like json schema's additionalProperties: false.

SteadBytes commented 5 years ago

A code example would be very useful @abadyan-vonage 😊

abadyan-vonage commented 5 years ago

@SteadBytes any kind of example would do here... account_settings_model = api.model("AccountSettings", {"autoLogout": fields.Integer()}) If I have an endpoint that expects this model:

@api.route("/accounts/<int:account_id>/settings")
class AccountSettings(Resource):
@api.expect(account_settings_model)
    def put(self, account_id: int):
      print(request.json)

When this endpoint gets called with a json like this: {"autoLogout": 4, "xxx": "yyy" } The xxx field does not get filtered out in any way or blocked.

I think the best approach would be to give me access to an object that only contains the fields from the model, since ideally I would not want to fail requests containing extra fields.

SteadBytes commented 5 years ago

Thank you for your example, api.expect is not intended to filter input payloads from request.json in this manner. It will validate that the fields provided match the model if validate=True is passed. As @j5awry mentioned, validation failure won't occur due to additional fields being present. However, it will fail if required fields are missing.

from flask import Flask, request

from flask_restplus import (
    Api,
    Namespace,
    Resource,
    fields,
    reqparse,
)

app = Flask(__name__)
api = Api(app)

account_settings_model = api.model(
    "AccountSettings",
    {
        "autoLogout": fields.Integer(required=True), # must be present for validation to pass
        "accountID": fields.String, # optional
    },
)

@api.route("/accounts/<int:account_id>/settings")
class AccountSettings(Resource):
    @api.expect(account_settings_model, validate=True) # ensure payload validation
    def put(self, account_id):
        print(request.json)

if __name__ == "__main__":
    app.run(port=8080, debug=True)

200 OK

All fields are present

{
    "autoLogout": 4,
    "accountID": "abcde"
}

Output in console:

{'autoLogout': 4, 'accountID': 'abcde'}

All fields are present, plus an additional field not defined in the model

{
    "autoLogout": 4,
    "accountID": "abcde",
    "someOtherField": 1234
}

Output in console:

{'autoLogout': 4, 'accountID': 'abcde', 'someOtherField': 1234}

All required fields present

{
    "autoLogout": 4,
}

Output in console:

{'autoLogout': 4}

All required fields present, plus an additional field not defined on the model

{
    "autoLogout": 4,
    "someOtherField": 1234
}

Output in console:

{'autoLogout': 4, 'someOtherField': 1234}

400 BAD REQUEST

Missing required field

{
    "accountID": "abcde"
}

Response body:

{
    "errors": {
        "autoLogout": "'autoLogout' is a required property"
    },
    "message": "Input payload validation failed"
}

Missing required field with additional field not defined on the model

{
    "accountID": "abcde",
    "someOtherField": 1234
}

Response body:

{
    "errors": {
        "autoLogout": "'autoLogout' is a required property"
    },
    "message": "Input payload validation failed"
}

Does that clarify the behaviour? :smile:

If you wish to filter the payload after it has been validated by api.expect you could use marshal within the body of the method:

from flask_restplus import marshal
...
    def put(self, account_id):
        filtered_input = marshal(request.json, account_settings_model)
        print(filtered_input)

Payload:

{
    "autoLogout": 4,
    "accountID": "abcde",
    "someOtherField": 1234
}

Output on console:

{'autoLogout': 4, 'accountID': 'abcde'}

abadyan-vonage commented 5 years ago

@SteadBytes Incredible, thanks for the detailed explanation. Actually, marshal was exactly what I needed, I missed the fact that it can be used that way. Thanks!

SteadBytes commented 5 years ago

Great, glad to help! :smile: I'm going to close this issue now :+1:

knowBalpreet commented 5 years ago

@SteadBytes marshal does not work when model is defined with json schema. Any way to make it work?

SteadBytes commented 5 years ago

Can you give a code example please? There are currently some unresolved issues going on around JSON schema models but without some code i can't say whether they're affecting you 😊

knowBalpreet commented 5 years ago

Sure, consider the following modal:


def field(type, **kwargs):
  return {"type": type, **kwargs}

account_model = NS.schema_model('account',
  {
      "type": "object",
      "required": ["name", "email", "userId"],
      "properties": {
          "name": field(
              'string',
              description="The name of the admin user for this account",
              minLength=1,
              example="Balpreet"
          ),
          "email": field(
              'string',
              description="The email of the admin user for this account",
              minLength=1,
              example="balpreet.s@tyroo.com"
          ),
          "userId": field(
              'string',
              description="The userId of the admin user for this account",
              minLength=1,
              example="2a714f61-fca2-4887-908c-03a9f75c0877"
          )
      }
  }
)

Now, when i do, data = marshal(request.json, account_model) i get the following error:

  File "/Users/knowbalpreet/Desktop/work/main/spector.api/app/Modules/Account/controller.py", line 36, in post
    return service.add_account(data=marshal(post_data, account))
  File "/Users/knowbalpreet/Desktop/work/main/spector.api/venv3/lib/python3.7/site-packages/flask_restplus/marshalling.py", line 58, in marshal
    out, has_wildcards = _marshal(data, fields, envelope, skip_none, mask, ordered)
  File "/Users/knowbalpreet/Desktop/work/main/spector.api/venv3/lib/python3.7/site-packages/flask_restplus/marshalling.py", line 180, in _marshal
    for k, v in iteritems(fields)
  File "/Users/knowbalpreet/Desktop/work/main/spector.api/venv3/lib/python3.7/site-packages/six.py", line 587, in iteritems
    return iter(d.items(**kw))
AttributeError: 'SchemaModel' object has no attribute 'items'