Skip to content

Commit

Permalink
Better @doc.security
Browse files Browse the repository at this point in the history
  • Loading branch information
endafarrell-herecom committed Jan 21, 2021
1 parent af140d8 commit fed8003
Show file tree
Hide file tree
Showing 14 changed files with 440 additions and 782 deletions.
24 changes: 18 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ including sanic path params. python 3.6+
[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/)
[![Downloads](https://pepy.tech/badge/sanic-openapi3e)](https://pepy.tech/project/sanic-openapi3e)

## Table of Contents
1. [Installation](#installation)
2. [Usage](#usage)
3. [Control spec generation](#Control-spec-generation)
4. [OAS Object maturity](#oas-object-maturity)
[comment]: <> (## Table of Contents)

[comment]: <> (1. [Installation]&#40;#installation&#41;)

[comment]: <> (2. [Usage]&#40;#usage&#41;)

[comment]: <> (3. [Control spec generation]&#40;#Control-spec-generation&#41;)

[comment]: <> (4. [OAS Object maturity]&#40;#oas-object-maturity&#41;)

## Installation

Expand Down Expand Up @@ -43,7 +47,11 @@ app.go_fast()
```

You'll now have a specification at the URL `/openapi/spec.json` and
a YAML version at `/openapi/spec.yml`.
a YAML version at `/openapi/spec.yml`.

p.s.: Clicking on the `/openapi/spec.yml` link in a browser will generally download a file and your computer will open
an application to read it. To see the YAML spec in the browser as a plain text file, add a `?as_text` query string like
`/openapi/spec.yml?as_text` instead.

Your routes will be automatically categorized by their blueprints'
names.
Expand Down Expand Up @@ -467,5 +475,9 @@ XML | beta | no known usage
specs.

## Changelog
* v0.9.3
* Adds a `@doc.security()` to override security requirements on a route.
* Removes entries with `false` values from the spec if `false` is the default value. This makes the specs smaller in
in size and are more idomatic.
* v0.9.2
* Fixes an issue of rendering SecurityRequirement when there were no entries in the list.
93 changes: 93 additions & 0 deletions examples/simple_06_security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import pathlib
import random
from typing import List

import sanic.request
import sanic.response
import sanic.router
from sanic import Sanic

# isort: off
# These two lines are to ensure that the version of `sanic_openapi3e` your app uses is from this checkout.
import sys

sys.path.insert(0, str(pathlib.Path(__file__).absolute().parent.parent))
from sanic_openapi3e import doc, openapi_blueprint, swagger_blueprint

# isort: on


int_min_4 = doc.Schema(_type="integer", _format="int32", minimum=4, description="Minimum value: 4")
int_min_4_ref = doc.Reference("#/components/schemas/int.min4")
an_id_ex1 = doc.Example(summary="A small number", description="Desc: Numbers less than ten", value=7)
an_id_ex2 = doc.Example(summary="A big number", description="Desc: Numbers more than one million!", value=123456789,)
days_of_week = doc.Schema(
_type="string",
description="Days of the week, short, English",
enum=["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
)

schemas = {
"str.min4": doc.Schema(title="str.min4", _type="string", minimum=4, description="A string of len >= 4",),
"int.min4": doc.Schema(title="int.min4", _type="integer", _format="int32", minimum=4, description="Minimum: 4",),
}
components = doc.Components(schemas=schemas)
security: List[doc.SecurityRequirement] = [doc.SecurityRequirement({"bearerAuth": []})]


app = Sanic(name=__file__, strict_slashes=True)
app.blueprint(openapi_blueprint)
app.blueprint(swagger_blueprint)

app.config.API_TITLE = __file__
app.config.API_DESCRIPTION = "This file has a simple example adding security "
app.config.OPENAPI_COMPONENTS = components
app.config.OPENAPI_SECURITY = security


@app.get("/object/<an_id:int>")
@doc.parameter(
name="an_id", description="An ID", required=True, _in="path", schema=doc.Schema.Integer,
)
@doc.tag("Tag 1", description="A tag desc")
@doc.security([])
def get_id(request, an_id: int):
d = locals()
del d["request"] # not JSON serializable
return sanic.response.json(d)


@app.head("/object/<an_id:int>")
@doc.parameter(name="an_id", description="An ID", required=True, _in="path", schema=int_min_4_ref)
@doc.response("200", description="You got a 200!", headers={"x-prize": doc.Header(description="free money")})
@doc.security([])
def head_id(request, an_id: int):
d = locals()
del d["request"] # not JSON serializable
return sanic.response.json(d)


@app.post("/object/")
def test_post(request: sanic.request.Request):
query = request.query_string
return sanic.response.json({"query": query, "id": random.randint(1, 100)})


@app.put("/object/<an_id:int>")
def put_id(request: sanic.request.Request, an_id: int):
d = locals()
del d["request"] # not JSON serializable
return sanic.response.json(d)


example_port = 8002


@app.listener("after_server_start")
async def notify_server_started(_, __):
print("\n\n************* sanic-openapi3e ********************************")
print(f"* See your openapi swagger on http://127.0.0.1:{example_port}/swagger/ *")
print("************* sanic-openapi3e ********************************\n\n")


app.go_fast(port=example_port)
2 changes: 1 addition & 1 deletion sanic_openapi3e/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
from .openapi import blueprint as openapi_blueprint
from .swagger import blueprint as swagger_blueprint

__version__ = "0.9.2"
__version__ = "0.9.3"
__all__ = ["openapi_blueprint", "swagger_blueprint", "doc"]
25 changes: 24 additions & 1 deletion sanic_openapi3e/doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from .oas_types import * # pylint: disable=unused-wildcard-import, wildcard-import # <<-- here for users

module_tags: Dict[str, Tag] = {}
endpoints: Paths = Paths()
endpoints: Paths = Paths() # Note: this is really a Dict[Callable, PathItem] under the hood.


def deprecated():
Expand Down Expand Up @@ -232,6 +232,29 @@ def inner(func):
return inner


def security(requirements: List[SecurityRequirement]):
"""
Lists the required security schemes to execute this operation. The name used for each property MUST correspond to a
security scheme declared in the Security Schemes under the Components Object.
Security Requirement Objects that contain multiple schemes require that all schemes MUST be satisfied for a request
to be authorized. This enables support for scenarios where multiple query parameters or HTTP headers are required to
convey security information.
When a list of Security Requirement Objects is defined on the OpenAPI Object or Operation Object, only one of the
Security Requirement Objects in the list needs to be satisfied to authorize the request.
`sanic-openapi3e hint: set your standard security requirements via `app.config.OPENAPI_SECURITY`; then override them
as needed with a `@doc.security` annotation. Use `@doc.security([])` to disable security for this route/operation.
"""

def inner(func):
endpoints[func].x_security_holder = requirements
return func

return inner


def servers(server_list: List[Server]):
"""
Add an alternative server array to service all operations in this path. Note that if you have not set the top-level
Expand Down
41 changes: 31 additions & 10 deletions sanic_openapi3e/oas_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,19 +179,38 @@ def as_yamlable_object( # pylint: disable=too-many-branches
raise TypeError(str(opt_key) + " " + self.__class__.__qualname__ + " " + repr(self))

for key, value in self.__dict__.items():
key2 = openapi_keyname(key)

# Schema classes have many keys with bool where their default is false. Don't send them out.
if self.__class__.__qualname__ == "Schema" and value is False:
if key2 in {
"nullable",
"readOnly",
"writeOnly",
" deprecated",
"exclusiveMaximum",
"exclusiveMinimum",
"uniqueItems",
}:
continue

# Allow False bools, but not other falsy values - UNLESS self is a SecurityRequirement
# Allow False bools, but not other falsy values - UNLESS:
# 1: self is a SecurityRequirement
# 2: self is an Operation and key is security
# as an empty list has special meaning for these.
if (value is not False) and (not value):
if value == [] and self.__class__.__qualname__ == "SecurityRequirement":
pass
elif value == [] and key == "security" and self.__class__.__qualname__ == "Operation":
pass
else:
continue
if key.startswith("x_"):
continue
if key == "deprecated" and value is False:
# By default, items in specs are `deprecated: false` - these are not desirable in the specs
continue
key2 = openapi_keyname(key)

value2: Union[Dict, List, str, bytes, int, float, bool]
if value is False or value is True:
value2 = value
Expand Down Expand Up @@ -3113,12 +3132,12 @@ def __init__( # pylint: disable=too-many-locals, too-many-arguments
servers: Optional[List[Server]] = None,
parameters: Optional[List[Union[Parameter, Reference]]] = None,
request_body: Optional[Union[RequestBody, Reference]] = None,
# TODO = add hide/suppress?
x_tags_holder: Optional[List[Tag]] = None,
x_deprecated_holder: bool = False,
x_exclude: bool = False,
x_external_docs_holder: Optional[ExternalDocumentation] = None,
x_responses_holder: Optional[Dict[str, Union[Response, Reference]]] = None,
x_exclude: bool = False,
x_security_holder: Optional[List[SecurityRequirement]] = None,
x_tags_holder: Optional[List[Tag]] = None,
):
"""
Describes the operations available on a single path. A Path Item MAY be empty, due to ACL constraints. The path
Expand Down Expand Up @@ -3146,18 +3165,19 @@ def __init__( # pylint: disable=too-many-locals, too-many-arguments
location. The list can use the Reference Object to link to parameters that are defined at the OpenAPI
Object's components/parameters.
:param request_body: sanic-openapi3e implementation extension to allow requestBody to be defined.
:param x_tags_holder: sanic-openapi3e implementation extension to allow tags on the PathItem until they can be
passed to the Operation/s.
:param x_deprecated_holder: sanic-openapi3e implementation extension to allow deprecated on the PathItem until
they can be passed to the Operation/s.
:param x_exclude: sanic-openapi3e extension to completly exclude the path from the spec.
:param x_external_docs_holder: sanic-openapi3e implementation extension to allow externalDocs on the PathItem
until they can be passed to the Operation/s.
:param x_responses_holder: sanic-openapi3e implementation extension to allow Responses on the PathItem until
they can be passed to the Operation/s.
:param x_exclude: sanic-openapi3e extension to completly exclude the path from the spec.
:param x_security_holder: sanic-openapi3e implementation extension to allow security on the PathItem until they
can be passed to the Operation/s.
:param x_tags_holder: sanic-openapi3e implementation extension to allow tags on the PathItem until they can be
passed to the Operation/s.
"""
if x_external_docs_holder:
raise ValueError(x_external_docs_holder)

# TODO - validations
if x_tags_holder:
Expand Down Expand Up @@ -3220,6 +3240,7 @@ def __init__( # pylint: disable=too-many-locals, too-many-arguments
self.request_body = request_body

self.x_tags_holder: List[Tag] = x_tags_holder if x_tags_holder is not None else []
self.x_security_holder = x_security_holder
self.x_deprecated_holder = x_deprecated_holder
self.x_responses_holder: Responses = Responses(x_responses_holder)
self.x_external_docs_holder = x_external_docs_holder
Expand Down
21 changes: 12 additions & 9 deletions sanic_openapi3e/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,9 +345,9 @@ def _buld_openapi_paths( # pylint: disable=too-many-arguments,too-many-locals,t
servers=path_item.servers,
summary=path_item_summary,
tags=sorted(pathitem_tag_names),
security=path_item.x_security_holder,
# TODO
callbacks=NOT_YET_IMPLEMENTED,
security=NOT_YET_IMPLEMENTED,
)

_path = PathItem(**operations)
Expand Down Expand Up @@ -502,8 +502,9 @@ async def spec_v3_json(_):


@blueprint.route("/spec.yml")
async def spec_v3_yaml(_):
return await serve_spec(_OPENAPI, "yaml")
async def spec_v3_yaml(request: sanic.request.Request):
as_text = "as_text" in request.query_string
return await serve_spec(_OPENAPI, "yaml", as_text)


@blueprint.route("/uncloaked.json")
Expand All @@ -512,8 +513,9 @@ async def spec_v3_uncloaked_json(_):


@blueprint.route("/uncloaked.yml")
async def spec_v3_uncloaked_yaml(_):
return await serve_spec(_OPENAPI_UNCLOAKED, "yaml")
async def spec_v3_uncloaked_yaml(request: sanic.request.Request):
as_text = "as_text" in request.query_string
return await serve_spec(_OPENAPI_UNCLOAKED, "yaml", as_text)


# ======================================================================================================================
Expand All @@ -526,14 +528,15 @@ async def spec_all_json(_):


@blueprint.route("/spec.all.yml")
async def spec_all_yml(_):
return await serve_spec(_OPENAPI_ALL, "yaml")
async def spec_all_yml(request: sanic.request.Request):
as_text = "as_text" in request.query_string
return await serve_spec(_OPENAPI_ALL, "yaml", as_text)


# ======================================================================================================================


async def serve_spec(spec: Dict, json_yaml: str):
async def serve_spec(spec: Dict, json_yaml: str, yaml_as_text: bool = False):
if not spec:
# ... including empty dicts in this if block
raise sanic.exceptions.NotFound("Not found")
Expand All @@ -542,6 +545,6 @@ async def serve_spec(spec: Dict, json_yaml: str):
return sanic.response.json(spec)

return sanic.response.HTTPResponse(
content_type=YAML_CONTENT_TYPE,
content_type="text/plain" if yaml_as_text else YAML_CONTENT_TYPE,
body=yaml.dump(spec, Dumper=yaml.CDumper, default_flow_style=False, explicit_start=False, sort_keys=False),
)
Loading

0 comments on commit fed8003

Please sign in to comment.