From 6ec11825b8cbcd9009ce91f762fe240d09a5cf7b Mon Sep 17 00:00:00 2001 From: krise3k Date: Mon, 12 Nov 2018 08:53:50 +0100 Subject: [PATCH 1/3] Add support for JWT authentication (#732) * Add support for JWT * Add example for JWT * Add minimal JWT documentation --- connexion/decorators/security.py | 70 ++++++++++++++++----- connexion/operations/secure.py | 29 ++++++--- docs/security.rst | 8 +++ examples/openapi3/jwt/README.rst | 14 +++++ examples/openapi3/jwt/app.py | 53 ++++++++++++++++ examples/openapi3/jwt/openapi.yaml | 45 +++++++++++++ examples/openapi3/jwt/requirements.txt | 4 ++ tests/api/test_secure_api.py | 5 +- tests/fakeapi/hello.py | 7 +++ tests/fixtures/secure_endpoint/openapi.yaml | 26 ++++++++ tests/fixtures/secure_endpoint/swagger.yaml | 28 +++++++++ 11 files changed, 265 insertions(+), 24 deletions(-) create mode 100644 examples/openapi3/jwt/README.rst create mode 100755 examples/openapi3/jwt/app.py create mode 100644 examples/openapi3/jwt/openapi.yaml create mode 100644 examples/openapi3/jwt/requirements.txt diff --git a/connexion/decorators/security.py b/connexion/decorators/security.py index 3034387ff..6413a316f 100644 --- a/connexion/decorators/security.py +++ b/connexion/decorators/security.py @@ -87,6 +87,21 @@ def get_apikeyinfo_func(security_definition): return None +def get_bearerinfo_func(security_definition): + """ + :type security_definition: dict + :rtype: function + + >>> get_bearerinfo_func({'x-bearerInfoFunc': 'foo.bar'}) + '' + """ + func = (security_definition.get("x-bearerInfoFunc") or + os.environ.get('BEARERINFO_FUNC')) + if func: + return get_function_from_name(func) + return None + + def security_passthrough(function): """ :type function: types.FunctionType @@ -137,26 +152,39 @@ def validate_scope(required_scopes, token_scopes): return True -def verify_oauth(token_info_func, scope_validate_func): - def wrapper(request, required_scopes): - authorization = request.headers.get('Authorization') - if not authorization: - return None +def verify_authorization_token(request, token_info_func): + """ + :param request: ConnexionRequest + :param token_info_func: types.FunctionType + :rtype: dict + """ + authorization = request.headers.get('Authorization') + if not authorization: + return None - try: - auth_type, token = authorization.split(None, 1) - except ValueError: - raise OAuthProblem(description='Invalid authorization header') + try: + auth_type, token = authorization.split(None, 1) + except ValueError: + raise OAuthProblem(description='Invalid authorization header') - if auth_type.lower() != 'bearer': - return None + if auth_type.lower() != 'bearer': + return None + + token_info = token_info_func(token) + if token_info is None: + raise OAuthResponseProblem( + description='Provided token is not valid', + token_response=None + ) - token_info = token_info_func(token) + return token_info + + +def verify_oauth(token_info_func, scope_validate_func): + def wrapper(request, required_scopes): + token_info = verify_authorization_token(request, token_info_func) if token_info is None: - raise OAuthResponseProblem( - description='Provided oauth token is not valid', - token_response=None - ) + return None # Fallback to 'scopes' for backward compability token_scopes = token_info.get('scope', token_info.get('scopes', '')) @@ -222,6 +250,16 @@ def wrapper(request, required_scopes): return wrapper +def verify_bearer(bearer_info_func): + """ + :param bearer_info_func: types.FunctionType + :rtype: types.FunctionType + """ + def wrapper(request, required_scopes): + return verify_authorization_token(request, bearer_info_func) + return wrapper + + def verify_security(auth_funcs, required_scopes, function): @functools.wraps(function) def wrapper(request): diff --git a/connexion/operations/secure.py b/connexion/operations/secure.py index 71b603aa0..af047cf32 100644 --- a/connexion/operations/secure.py +++ b/connexion/operations/secure.py @@ -4,10 +4,11 @@ from ..decorators.decorator import (BeginOfRequestLifecycleDecorator, EndOfRequestLifecycleDecorator) from ..decorators.security import (get_apikeyinfo_func, get_basicinfo_func, + get_bearerinfo_func, get_scope_validate_func, get_tokeninfo_func, security_deny, security_passthrough, - verify_apikey, verify_basic, verify_oauth, - verify_security) + verify_apikey, verify_basic, verify_bearer, + verify_oauth, verify_security) logger = logging.getLogger("connexion.operations.secure") @@ -118,16 +119,30 @@ def security_decorator(self): continue auth_funcs.append(verify_basic(basic_info_func)) + elif scheme == 'bearer': + bearer_info_func = get_bearerinfo_func(security_scheme) + if not bearer_info_func: + logger.warning("... x-bearerInfoFunc missing", extra=vars(self)) + continue + auth_funcs.append(verify_bearer(bearer_info_func)) else: logger.warning("... Unsupported http authorization scheme %s" % scheme, extra=vars(self)) elif security_scheme['type'] == 'apiKey': - apikey_info_func = get_apikeyinfo_func(security_scheme) - if not apikey_info_func: - logger.warning("... x-apikeyInfoFunc missing", extra=vars(self)) - continue + scheme = security_scheme.get('x-authentication-scheme', '').lower() + if scheme == 'bearer': + bearer_info_func = get_bearerinfo_func(security_scheme) + if not bearer_info_func: + logger.warning("... x-bearerInfoFunc missing", extra=vars(self)) + continue + auth_funcs.append(verify_bearer(bearer_info_func)) + else: + apikey_info_func = get_apikeyinfo_func(security_scheme) + if not apikey_info_func: + logger.warning("... x-apikeyInfoFunc missing", extra=vars(self)) + continue - auth_funcs.append(verify_apikey(apikey_info_func, security_scheme['in'], security_scheme['name'])) + auth_funcs.append(verify_apikey(apikey_info_func, security_scheme['in'], security_scheme['name'])) else: logger.warning("... Unsupported security scheme type %s" % security_scheme['type'], extra=vars(self)) diff --git a/docs/security.rst b/docs/security.rst index 5045f8f32..8874c3b11 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -68,6 +68,14 @@ parameters: apikey and required_scopes. You can find a `minimal Basic Auth example application`_ in Connexion's "examples" folder. +Bearer Authentication (JWT) +--------------------------- + +With Connexion, the API security definition **must** include a +``x-bearerInfoFunc`` or set ``BEARERINFO_FUNC`` env var. It uses the same +semantics as for ``x-tokenInfoFunc``, but the function accepts one parameter: token. + +You can find a `minimal JWT example application`_ in Connexion's "examples/openapi3" folder. HTTPS Support ------------- diff --git a/examples/openapi3/jwt/README.rst b/examples/openapi3/jwt/README.rst new file mode 100644 index 000000000..9d0bde123 --- /dev/null +++ b/examples/openapi3/jwt/README.rst @@ -0,0 +1,14 @@ +======================= +JWT Auth Example +======================= + +Running: + +.. code-block:: bash + + $ sudo pip3 install -r requirements.txt + $ ./app.py + +Now open your browser and go to http://localhost:8080/ui/ to see the Swagger UI. +Use endpoint **/auth** to generate JWT token, copy it, then click **Authorize** button and paste the token. +Now you can use endpoint **/secret** to check autentication. \ No newline at end of file diff --git a/examples/openapi3/jwt/app.py b/examples/openapi3/jwt/app.py new file mode 100755 index 000000000..46a243f71 --- /dev/null +++ b/examples/openapi3/jwt/app.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +''' +Basic example of a resource server +''' + +import time + +import connexion +import six +from werkzeug.exceptions import Unauthorized + +from jose import JWTError, jwt + +JWT_ISSUER = 'com.zalando.connexion' +JWT_SECRET = 'change_this' +JWT_LIFETIME_SECONDS = 600 +JWT_ALGORITHM = 'HS256' + + +def generate_token(user_id): + timestamp = _current_timestamp() + payload = { + "iss": JWT_ISSUER, + "iat": int(timestamp), + "exp": int(timestamp + JWT_LIFETIME_SECONDS), + "sub": str(user_id), + } + + return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM) + + +def decode_token(token): + try: + return jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM]) + except JWTError as e: + six.raise_from(Unauthorized, e) + + +def get_secret(user, token_info) -> str: + return ''' + You are user_id {user} and the secret is 'wbevuec'. + Decoded token claims: {token_info}. + '''.format(user=user, token_info=token_info) + + +def _current_timestamp() -> int: + return int(time.time()) + + +if __name__ == '__main__': + app = connexion.FlaskApp(__name__) + app.add_api('openapi.yaml') + app.run(port=8080) diff --git a/examples/openapi3/jwt/openapi.yaml b/examples/openapi3/jwt/openapi.yaml new file mode 100644 index 000000000..71af5cc69 --- /dev/null +++ b/examples/openapi3/jwt/openapi.yaml @@ -0,0 +1,45 @@ +openapi: 3.0.0 +info: + title: JWT Example + version: '1.0' +paths: + /auth/{user_id}: + get: + summary: Return JWT token + operationId: app.generate_token + parameters: + - name: user_id + description: User unique identifier + in: path + required: true + example: 12 + schema: + type: integer + responses: + '200': + description: JWT token + content: + 'text/plain': + schema: + type: string + /secret: + get: + summary: Return secret string + operationId: app.get_secret + responses: + '200': + description: secret response + content: + 'text/plain': + schema: + type: string + security: + - jwt: ['secret'] + +components: + securitySchemes: + jwt: + type: http + scheme: bearer + bearerFormat: JWT + x-bearerInfoFunc: app.decode_token diff --git a/examples/openapi3/jwt/requirements.txt b/examples/openapi3/jwt/requirements.txt new file mode 100644 index 000000000..de5ea7575 --- /dev/null +++ b/examples/openapi3/jwt/requirements.txt @@ -0,0 +1,4 @@ +connexion>=2.0.0rc3 +python-jose[cryptography] +six>=1.9 +Flask>=0.10.1 diff --git a/tests/api/test_secure_api.py b/tests/api/test_secure_api.py index d77c43e17..e21501cfc 100644 --- a/tests/api/test_secure_api.py +++ b/tests/api/test_secure_api.py @@ -60,7 +60,7 @@ def test_security(oauth_requests, secure_endpoint_app): assert get_bye_bad_token.content_type == 'application/problem+json' get_bye_bad_token_reponse = json.loads(get_bye_bad_token.data.decode('utf-8', 'replace')) # type: dict assert get_bye_bad_token_reponse['title'] == 'Unauthorized' - assert get_bye_bad_token_reponse['detail'] == "Provided oauth token is not valid" + assert get_bye_bad_token_reponse['detail'] == "Provided token is not valid" response = app_client.get('/v1.0/more-than-one-security-definition') # type: flask.Response assert response.status_code == 401 @@ -84,6 +84,9 @@ def test_security(oauth_requests, secure_endpoint_app): get_bye_from_connexion = app_client.get('/v1.0/byesecure-from-connexion', headers=headers) # type: flask.Response assert get_bye_from_connexion.data == b'Goodbye test-user (Secure!)' + headers = {"Authorization": "Bearer 100"} + get_bye_from_connexion = app_client.get('/v1.0/byesecure-jwt/test-user', headers=headers) # type: flask.Response + assert get_bye_from_connexion.data == b'Goodbye test-user (Secure: 100)' def test_checking_that_client_token_has_all_necessary_scopes( oauth_requests, secure_endpoint_app): diff --git a/tests/fakeapi/hello.py b/tests/fakeapi/hello.py index 441b45eb9..9d7b6a359 100755 --- a/tests/fakeapi/hello.py +++ b/tests/fakeapi/hello.py @@ -88,6 +88,8 @@ def get_bye_secure_from_connexion(req_context): def get_bye_secure_ignoring_context(name): return 'Goodbye {name} (Secure!)'.format(name=name) +def get_bye_secure_jwt(name, user, token_info): + return 'Goodbye {name} (Secure: {user})'.format(name=name, user=user) def with_problem(): return problem(type='http://www.example.com/error', @@ -499,3 +501,8 @@ def apikey_info(apikey, required_scopes=None): if apikey == 'mykey': return {'sub': 'admin'} return None + +def jwt_info(token): + if token == '100': + return {'sub': '100'} + return None diff --git a/tests/fixtures/secure_endpoint/openapi.yaml b/tests/fixtures/secure_endpoint/openapi.yaml index b90342e0a..b41499d87 100644 --- a/tests/fixtures/secure_endpoint/openapi.yaml +++ b/tests/fixtures/secure_endpoint/openapi.yaml @@ -77,6 +77,27 @@ paths: required: true schema: type: string + '/byesecure-jwt/{name}': + get: + summary: Generate goodbye + description: Generates a goodbye message. + operationId: fakeapi.hello.get_bye_secure_jwt + security: + - jwt: [] + responses: + '200': + description: goodbye response + content: + text/plain: + schema: + type: string + parameters: + - name: name + in: path + description: Name of the person to say bye. + required: true + schema: + type: string /more-than-one-security-definition: get: summary: Some external call to API @@ -121,3 +142,8 @@ components: name: X-Auth in: header x-apikeyInfoFunc: fakeapi.hello.apikey_info + jwt: + type: http + scheme: bearer + bearerFormat: JWT + x-bearerInfoFunc: fakeapi.hello.jwt_info diff --git a/tests/fixtures/secure_endpoint/swagger.yaml b/tests/fixtures/secure_endpoint/swagger.yaml index b5ac625c1..2fa1d333a 100644 --- a/tests/fixtures/secure_endpoint/swagger.yaml +++ b/tests/fixtures/secure_endpoint/swagger.yaml @@ -22,6 +22,13 @@ securityDefinitions: in: header x-apikeyInfoFunc: fakeapi.hello.apikey_info + jwt: + type: apiKey + name: Authorization + in: header + x-authentication-scheme: Bearer + x-bearerInfoFunc: fakeapi.hello.jwt_info + paths: /byesecure/{name}: get: @@ -99,6 +106,27 @@ paths: required: true type: string + /byesecure-jwt/: + get: + summary: Generate goodbye + description: "" + operationId: fakeapi.hello.get_bye_secure_jwt + security: + - jwt: [] + produces: + - text/plain + responses: + 200: + description: goodbye response + schema: + type: string + parameters: + - name: name + in: path + description: Name of the person to say bye. + required: true + type: string + /more-than-one-security-definition: get: summary: Some external call to API From ddd5eeb5cb30d1eb31c8ed1506c0b5581151fae3 Mon Sep 17 00:00:00 2001 From: Daniel Grossmann-Kavanagh Date: Mon, 12 Nov 2018 18:15:58 +1000 Subject: [PATCH 2/3] rename bandit.yaml to bandit.yml (#773) Fixes #679 --- bandit.yaml => bandit.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename bandit.yaml => bandit.yml (100%) diff --git a/bandit.yaml b/bandit.yml similarity index 100% rename from bandit.yaml rename to bandit.yml From 343643428cf68b9c4bfba21fe3df162170c68b48 Mon Sep 17 00:00:00 2001 From: Daniel Grossmann-Kavanagh Date: Mon, 12 Nov 2018 18:40:42 +1000 Subject: [PATCH 3/3] update documentation (#771) --- README.rst | 83 ++++++++++++++++++++++++++++++++------------- docs/cookbook.rst | 4 +-- docs/index.rst | 6 ++-- docs/quickstart.rst | 24 ++++++------- docs/request.rst | 71 +++++++++++++++++++++++++++++++++++--- docs/routing.rst | 34 ++++++++++++++----- 6 files changed, 169 insertions(+), 53 deletions(-) diff --git a/README.rst b/README.rst index 17af23952..916677c56 100644 --- a/README.rst +++ b/README.rst @@ -264,6 +264,12 @@ In this example, Connexion automatically recognizes that your view function expects an argument named ``message`` and assigns the value of the endpoint parameter ``message`` to your view function. +.. note:: In the OpenAPI 3.x.x spec, the requestBody does not have a name. + By default it will be passed in as 'body'. You can optionally + provide the x-body-name parameter in your requestBody schema + to override the name of the parameter that will be passed to your + handler function. + .. warning:: When you define a parameter at your endpoint as *not* required, and this argument does not have default value in your Python view, you will get a "missing positional argument" exception whenever you call this endpoint @@ -278,7 +284,7 @@ do type casting to related Python native values. The current available type castings are: +--------------+-------------+ -| Swagger Type | Python Type | +| OpenAPI Type | Python Type | +==============+=============+ | integer | int | +--------------+-------------+ @@ -290,6 +296,8 @@ available type castings are: +--------------+-------------+ | array | list | +--------------+-------------+ +| null | None | ++--------------+-------------+ | object | dict | +--------------+-------------+ @@ -299,9 +307,9 @@ supports collection formats "pipes" and "csv". The default format is "csv". Connexion is opinionated about how the URI is parsed for ``array`` types. The default behavior for query parameters that have been defined multiple -times is to join them all together. For example, if you provide a URI with +times is to use the right-most value. For example, if you provide a URI with the the query string ``?letters=a,b,c&letters=d,e,f``, connexion will set -``letters = ['a', 'b', 'c', 'd', 'e', 'f']``. +``letters = ['d', 'e', 'f']``. You can override this behavior by specifying the URI parser in the app or api options. @@ -309,20 +317,25 @@ api options. .. code-block:: python from connexion.decorators.uri_parsing import Swagger2URIParser - options = {'uri_parsing_class': Swagger2URIParser} + options = {'uri_parsing_class': AlwaysMultiURIParser} app = connexion.App(__name__, specification_dir='swagger/', options=options) You can implement your own URI parsing behavior by inheriting from ``connextion.decorators.uri_parsing.AbstractURIParser``. -There are three URI parsers included with connection. +There are a handful of URI parsers included with connection. +----------------------+---------------------------------------------------------------------------+ -| AlwaysMultiURIParser | This parser is backwards compatible, and joins together multiple instances| -| (default) | of the same query parameter. | +| OpenAPIURIParser | This parser adheres to the OpenAPI 3.x.x spec, and uses the ``style`` | +| default: OpenAPI 3.0 | parameter. Query parameters are parsed from left to right, so if a query | +| | parameter is defined twice, then the right-most definition will take | +| | precedence. For example, if you provided a URI with the query string | +| | ``?letters=a,b,c&letters=d,e,f``, and ``style: simple``, then connexion | +| | will set ``letters = ['d', 'e', 'f']``. For additional information see | +| | `OpenAPI 3.0 Style Values`_. | +----------------------+---------------------------------------------------------------------------+ | Swagger2URIParser | This parser adheres to the Swagger 2.0 spec, and will only join together | -| | multiple instance of the same query parameter if the ``collectionFormat`` | +| default: OpenAPI 2.0 | multiple instance of the same query parameter if the ``collectionFormat`` | | | is set to ``multi``. Query parameters are parsed from left to right, so | | | if a query parameter is defined twice, then the right-most definition | | | wins. For example, if you provided a URI with the query string | @@ -334,6 +347,9 @@ There are three URI parsers included with connection. | | string ``?letters=a,b,c&letters=d,e,f`` and ``collectionFormat: csv`` | | | hen connexion will set ``letters = ['a', 'b', 'c']`` | +----------------------+---------------------------------------------------------------------------+ +| AlwaysMultiURIParser | This parser is backwards compatible with Connexion 1.x. It joins together | +| | multiple instances of the same query parameter. | ++----------------------+---------------------------------------------------------------------------+ Parameter validation @@ -351,18 +367,33 @@ to your application: API Versioning and basePath --------------------------- -You can also define a ``basePath`` on the top level of the API -specification. This is useful for versioned APIs. To serve the -previous endpoint from ``http://MYHOST/1.0/hello_world``, type: +Setting a base path is useful for versioned APIs. An example of +a base path would be the ``1.0`` in ``http://MYHOST/1.0/hello_world``. + +If you are using OpenAPI 3.x.x, you set your base URL path in the +servers block of the specification. You can either specify a full +URL, or just a relative path. + +.. code-block:: yaml + + servers: + - url: https://MYHOST/1.0 + description: full url example + - url: /1.0 + description: relative path example + + paths: + ... + +If you are using OpenAPI 2.0, you can define a ``basePath`` on the top level +of your OpenAPI 2.0 specification. .. code-block:: yaml basePath: /1.0 paths: - /hello_world: - post: - operationId: myapp.api.hello_world + ... If you don't want to include the base path in your specification, you can provide it when adding the API to your application: @@ -374,22 +405,27 @@ can provide it when adding the API to your application: Swagger JSON ------------ Connexion makes the OpenAPI/Swagger specification in JSON format -available from ``swagger.json`` in the base path of the API. +available from either ``swagger.json`` (for OpenAPI 2.0) or +``openapi.json`` (for OpenAPI 3.x.x) at the base path of the API. +For example, if your base path was ``1.0``, then your spec would be +available at ``/1.0/openapi.json``. -You can disable the Swagger JSON at the application level: +You can disable serving the spec JSON at the application level: .. code-block:: python - app = connexion.App(__name__, specification_dir='swagger/', - swagger_json=False) + options = {"serve_spec": False} + app = connexion.App(__name__, specification_dir='openapi/', + options=options) app.add_api('my_api.yaml') You can also disable it at the API level: .. code-block:: python - app = connexion.App(__name__, specification_dir='swagger/') - app.add_api('my_api.yaml', swagger_json=False) + options = {"serve_spec": False} + app = connexion.App(__name__, specification_dir='openapi/') + app.add_api('my_api.yaml', options=options) HTTPS Support ------------- @@ -428,7 +464,7 @@ You can disable the Swagger UI at the application level: .. code-block:: python - app = connexion.App(__name__, specification_dir='swagger/', + app = connexion.App(__name__, specification_dir='openapi/', options={"swagger_ui": False}) app.add_api('my_api.yaml') @@ -437,7 +473,7 @@ You can also disable it at the API level: .. code-block:: python - app = connexion.App(__name__, specification_dir='swagger/') + app = connexion.App(__name__, specification_dir='openapi/') app.add_api('my_api.yaml', options={"swagger_ui": False}) If necessary, you can explicitly specify the path to the directory with @@ -447,7 +483,7 @@ In order to do this, you should specify the following option: .. code-block:: python options = {'swagger_path': '/path/to/swagger_ui/'} - app = connexion.App(__name__, specification_dir='swagger/', options=options) + app = connexion.App(__name__, specification_dir='openapi/', options=options) If you wish to provide your own swagger-ui distro, note that connextion expects a jinja2 file called ``swagger_ui/index.j2`` in order to load the @@ -568,6 +604,7 @@ Unless required by applicable law or agreed to in writing, software distributed .. _Jinja2: http://jinja.pocoo.org/ .. _rfc6750: https://tools.ietf.org/html/rfc6750 .. _OpenAPI Specification: https://www.openapis.org/ +.. _OpenAPI 3.0 Style Values: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#style-values .. _Operation Object: https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#operation-object .. _swager.spec.security_definition: https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#security-definitions-object .. _swager.spec.security_requirement: https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#security-requirement-object diff --git a/docs/cookbook.rst b/docs/cookbook.rst index ad482ee04..4019a1422 100644 --- a/docs/cookbook.rst +++ b/docs/cookbook.rst @@ -12,9 +12,9 @@ by the Connexion payload validation on request parameters and response payloads of your API. Let's say your API deals with Products and you want to define a field -`price_label` that have a "money" format value. You can create a format +`price_label` that has a "money" format value. You can create a format checker function and register that to be used to validate values of -"money" format. +the "money" format. Example of a possible schema of Product having an attribute with "money" format that would be defined in your OpenAPI specification: diff --git a/docs/index.rst b/docs/index.rst index 4b58eb037..e6e448cfa 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,8 +7,8 @@ Welcome to Connexion's documentation! ===================================== Connexion is a framework on top of Flask_ that automagically handles -HTTP requests based on `OpenAPI 2.0 Specification`_ (formerly known as -Swagger Spec) of your API described in `YAML format`_. Connexion +HTTP requests based on either the `OpenAPI 2.0 Specification`_ (formerly known +as Swagger Spec) or the `OpenAPI 3.0 Specification`_. Connexion allows you to write a Swagger specification and then maps the endpoints to your Python functions. This is what makes it unique from other tools that generate the specification based on your Python @@ -35,4 +35,4 @@ Contents: .. _Flask: http://flask.pocoo.org/ .. _OpenAPI 2.0 Specification: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md -.. _YAML format: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#format +.. _OpenAPI 3.0 Specification: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 12531ff26..7d5ace65e 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -14,19 +14,19 @@ In your command line, type this: .. code-block:: bash - $ pip install connexion + $ pip install connexion[swagger-ui] Running It ---------- -Put your API YAML inside a folder in the root path of your application (e.g ``swagger\``) and then do +Put your API YAML inside a folder in the root path of your application (e.g ``openapi\``) and then do .. code-block:: python import connexion - app = connexion.FlaskApp(__name__, specification_dir='swagger/') + app = connexion.FlaskApp(__name__, specification_dir='openapi/') app.add_api('my_api.yaml') app.run(port=8080) @@ -41,7 +41,7 @@ for each specific API in the `connexion.App#add_api` method: .. code-block:: python - app = connexion.FlaskApp(__name__, specification_dir='swagger/', + app = connexion.FlaskApp(__name__, specification_dir='openapi/', arguments={'global': 'global_value'}) app.add_api('my_api.yaml', arguments={'api_local': 'local_value'}) app.run(port=8080) @@ -58,8 +58,9 @@ You can disable the Swagger UI at the application level: .. code-block:: python - app = connexion.FlaskApp(__name__, specification_dir='swagger/', - swagger_ui=False) + options = {"swagger_ui": False} + app = connexion.FlaskApp(__name__, specification_dir='openapi/', + options=options) app.add_api('my_api.yaml') @@ -67,8 +68,9 @@ You can also disable it at the API level: .. code-block:: python - app = connexion.FlaskApp(__name__, specification_dir='swagger/') - app.add_api('my_api.yaml', swagger_ui=False) + options = {"swagger_ui": False} + app = connexion.FlaskApp(__name__, specification_dir='openapi/') + app.add_api('my_api.yaml', options=options) Server Backend -------------- @@ -79,7 +81,7 @@ to ``tornado`` or ``gevent``: import connexion - app = connexion.FlaskApp(__name__, port = 8080, specification_dir='swagger/', server='tornado') + app = connexion.FlaskApp(__name__, port = 8080, specification_dir='openapi/', server='tornado') Connexion has the ``aiohttp`` framework as server backend too: @@ -88,11 +90,9 @@ Connexion has the ``aiohttp`` framework as server backend too: import connexion - app = connexion.AioHttpApp(__name__, port = 8080, specification_dir='swagger/') + app = connexion.AioHttpApp(__name__, port = 8080, specification_dir='openapi/') .. _Jinja2: http://jinja.pocoo.org/ -.. _swagger.spec: https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md -.. _swagger.spec.operation: https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#operation-object .. _Tornado: http://www.tornadoweb.org/en/stable/ .. _gevent: http://www.gevent.org/ diff --git a/docs/request.rst b/docs/request.rst index d8ba56fb9..fbd01eb42 100644 --- a/docs/request.rst +++ b/docs/request.rst @@ -54,6 +54,12 @@ of the endpoint parameter `message` to your view function. Connexion will also use default values if they are provided. +.. note:: In the OpenAPI 3.x.x spec, the requestBody does not have a name. + By default it will be passed in as 'body'. You can optionally + provide the x-body-name parameter in your requestBody schema + to override the name of the parameter that will be passed to your + handler function. + .. warning:: Please note that when you have a parameter defined as *not* required at your endpoint and your Python view have a non-named argument, when you call this endpoint WITHOUT @@ -67,7 +73,7 @@ do type casting to related Python natives values. The current available type castings are: +--------------+-------------+ -| Swagger Type | Python Type | +| OpenAPI Type | Python Type | | | | +==============+=============+ | integer | int | @@ -80,18 +86,73 @@ available type castings are: +--------------+-------------+ | array | list | +--------------+-------------+ +| null | None | ++--------------+-------------+ | object | dict | +--------------+-------------+ -In the Swagger definition if the `array` type is used you can define the -`collectionFormat` that it should be recognized. Connexion currently +In the OpenAPI 2.0 specification, if the `array` type is used you can define the +`collectionFormat` used to deserialize the input. Connexion currently supports collection formats "pipes" and "csv". The default format is "csv". .. note:: For more details about `collectionFormat`'s please check the - official `Swagger/OpenAPI specification`_. + official `OpenAPI 2.0 Specification`_. + + +In the `OpenAPI 2.0 Specification`_ if you use the ``array`` type, +you can define the ``collectionFormat`` do set the deserialization behavior. +Connexion currently supports "pipes" and "csv" as collection formats. +The default format is "csv". + +Connexion is opinionated about how the URI is parsed for ``array`` types. +The default behavior for query parameters that have been defined multiple +times is to join them all together. For example, if you provide a URI with +the the query string ``?letters=a,b,c&letters=d,e,f``, connexion will set +``letters = ['a', 'b', 'c', 'd', 'e', 'f']``. + +You can override this behavior by specifying the URI parser in the app or +api options. + +.. code-block:: python + + from connexion.decorators.uri_parsing import Swagger2URIParser + options = {'uri_parsing_class': Swagger2URIParser} + app = connexion.App(__name__, specification_dir='swagger/', options=options) + +You can implement your own URI parsing behavior by inheriting from +``connextion.decorators.uri_parsing.AbstractURIParser``. + +There are a handful of URI parsers included with connection. + ++----------------------+---------------------------------------------------------------------------+ +| OpenAPIURIParser | This parser adheres to the OpenAPI 3.x.x spec, and uses the ``style`` | +| default: OpenAPI 3.0 | parameter. Query parameters are parsed from left to right, so if a query | +| | parameter is defined twice, then the right-most definition will take | +| | precedence. For example, if you provided a URI with the query string | +| | ``?letters=a,b,c&letters=d,e,f``, and ``style: simple``, then connexion | +| | will set ``letters = ['d', 'e', 'f']``. For additional information see | +| | `OpenAPI 3.0 Style Values`_. | ++----------------------+---------------------------------------------------------------------------+ +| Swagger2URIParser | This parser adheres to the Swagger 2.0 spec, and will only join together | +| default: OpenAPI 2.0 | multiple instance of the same query parameter if the ``collectionFormat`` | +| | is set to ``multi``. Query parameters are parsed from left to right, so | +| | if a query parameter is defined twice, then the right-most definition | +| | wins. For example, if you provided a URI with the query string | +| | ``?letters=a,b,c&letters=d,e,f``, and ``collectionFormat: csv``, then | +| | connexion will set ``letters = ['d', 'e', 'f']`` | ++----------------------+---------------------------------------------------------------------------+ +| FirstValueURIParser | This parser behaves like the Swagger2URIParser, except that it prefers | +| | the first defined value. For example, if you provided a URI with the query| +| | string ``?letters=a,b,c&letters=d,e,f`` and ``collectionFormat: csv`` | +| | hen connexion will set ``letters = ['a', 'b', 'c']`` | ++----------------------+---------------------------------------------------------------------------+ +| AlwaysMultiURIParser | This parser is backwards compatible with Connexion 1.x. It joins together | +| | multiple instances of the same query parameter. | ++----------------------+---------------------------------------------------------------------------+ + .. _jsonschema: https://pypi.python.org/pypi/jsonschema -.. _`Swagger/OpenAPI specification`: https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/2.0.md#fixed-fields-7 +.. _`OpenAPI 2.0 Specification`: https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/2.0.md#fixed-fields-7 Parameter validation ^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/routing.rst b/docs/routing.rst index 9d6b77aca..5e0baf82b 100644 --- a/docs/routing.rst +++ b/docs/routing.rst @@ -30,7 +30,10 @@ If you provided this path in your specification POST requests to x-swagger-router-controller: myapp.api operationId: hello_world -Keep in mind that Connexion follows how `HTTP methods work in Flask`_ and therefore HEAD requests will be handled by the ``operationId`` specified under GET in the specification. If both methods are supported, ``connexion.request.method`` can be used to determine which request was made. +Keep in mind that Connexion follows how `HTTP methods work in Flask`_ +and therefore HEAD requests will be handled by the ``operationId`` specified +under GET in the specification. If both methods are supported, +``connexion.request.method`` can be used to determine which request was made. Automatic Routing ----------------- @@ -120,21 +123,36 @@ requests to include forward slashes in the ``name`` url variable. API Versioning and basePath --------------------------- -You can also define a ``basePath`` on the top level of the API -specification. This is useful for versioned APIs. To serve the -previous endpoint from ``http://MYHOST/1.0/hello_world``, type: +Setting a base path is useful for versioned APIs. An example of +a base path would be the ``1.0`` in ``http://MYHOST/1.0/hello_world``. + +If you are using OpenAPI 3.x.x, you set your base URL path in the +servers block of the specification. You can either specify a full +URL, or just a relative path. + +.. code-block:: yaml + + servers: + - url: https://MYHOST/1.0 + description: full url example + - url: /1.0 + description: relative path example + + paths: + ... + +If you are using OpenAPI 2.0, you can define a ``basePath`` on the top level +of your OpenAPI 2.0 specification. .. code-block:: yaml basePath: /1.0 paths: - /hello_world: - post: - operationId: myapp.api.hello_world + ... If you don't want to include the base path in your specification, you -can just provide it when adding the API to your application: +can provide it when adding the API to your application: .. code-block:: python