Repository: marshmallow-code/apispec
Pull request #375 (2019-02-01 11:22:13)

Add component.security_scheme
Addresses https://github.com/marshmallow-code/apispec/issues/245.

https://swagger.io/docs/specification/components/
https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#componentsObject
https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#componentsObject

https://swagger.io/docs/specification/authentication/
https://swagger.io/docs/specification/2-0/authentication/


Patch:
diff --git a/apispec/core.py b/apispec/core.py
index 147c612..3f1c047 100644
--- a/apispec/core.py
+++ b/apispec/core.py
@@ -92,13 +92,20 @@ class Components(object):
         self._schemas = {}
         self._parameters = {}
         self._responses = {}
+        self._security_schemes = {}
 
     def to_dict(self):
-        schemas_key = 'definitions' if self.openapi_version.major < 3 else 'schemas'
+        if self.openapi_version.major < 3:
+            schemas_key = 'definitions'
+            security_key = 'securityDefinitions'
+        else:
+            schemas_key = 'schemas'
+            security_key = 'securitySchemes'
         return {
             'parameters': self._parameters,
             'responses': self._responses,
             schemas_key: self._schemas,
+            security_key: self._security_schemes,
         }
 
     def schema(
@@ -187,6 +194,19 @@ class Components(object):
         self._responses[ref_id] = ret
         return self
 
+    def security_scheme(self, sec_id, **kwargs):
+        """Add a security scheme which can be referenced.
+
+        :param str sec_id: sec_id to use as reference
+        :param dict kwargs: security scheme fields
+        """
+        if sec_id in self._security_schemes:
+            raise DuplicateComponentNameError(
+                'Another security scheme with name "{}" is already registered.'.format(sec_id),
+            )
+        self._security_schemes[sec_id] = kwargs
+        return self
+
 
 class APISpec(object):
     """Stores metadata that describes a RESTful API using the OpenAPI specification.
diff --git a/docs/special_topics.rst b/docs/special_topics.rst
index fef7d36..51a94d6 100644
--- a/docs/special_topics.rst
+++ b/docs/special_topics.rst
@@ -54,7 +54,7 @@ Documenting Top-level Components
 
 The ``APISpec`` object contains helpers to add top-level components.
 
-To add a schema (a.k.a. "definition" in OpenAPI v2 termionology), use
+To add a schema (f.k.a. "definition" in OAS v2), use
 `spec.components.schema <apispec.core.Components.schema>`.
 
 Likewise, parameters and responses can be added using
@@ -63,17 +63,17 @@ Likewise, parameters and responses can be added using
 
 To add other top-level objects, pass them to the ``APISpec`` as keyword arguments.
 
-Here is an example that includes a `Security Scheme Object <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#securitySchemeObject>`_.
+Here is an example that includes a `Server Object <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#serverObject>`_.
 
 .. code-block:: python
 
     import yaml
     from apispec import APISpec
     from apispec.ext.marshmallow import MarshmallowPlugin
-    from apispec.utils import validate_swagger
+    from apispec.utils import validate_spec
 
     OPENAPI_SPEC = """
-    openapi: 3.0.1
+    openapi: 3.0.2
     info:
       description: Server API document
       title: Server API
@@ -87,12 +87,6 @@ Here is an example that includes a `Security Scheme Object <https://github.com/O
           - '3000'
           - '8888'
           default: '3000'
-    components:
-      securitySchemes:
-        bearerAuth:
-          type: http
-          scheme: bearer
-          bearerFormat: JWT
     """
 
     settings = yaml.safe_load(OPENAPI_SPEC)
@@ -111,10 +105,37 @@ Here is an example that includes a `Security Scheme Object <https://github.com/O
         **settings
     )
 
-    validate_swagger(spec)
+    validate_spec(spec)
 
 
 When adding components, the main advantage of using dedicated methods over
 passing them as kwargs is the ability to use plugin helpers. For instance,
 `MarshmallowPlugin <apispec.ext.marshmallow.MarshmallowPlugin>` has helpers to
 resolve schemas in parameters and responses.
+
+Documenting Security Schemes
+----------------------------
+
+Use `spec.components.security_scheme <apispec.core.Components.security_scheme>`
+to document `Security Scheme Objects <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#securitySchemeObject>`_.
+
+.. code-block:: python
+
+    from pprint import pprint
+    from apispec import APISpec
+
+    spec = APISpec(
+        title='Swagger Petstore',
+        version='1.0.0',
+        openapi_version='3.0.2',
+    )
+
+    api_key_scheme = {'type': 'apiKey', 'in': 'header', 'name': 'X-API-Key'}
+    jwt_scheme = {'type': 'http', 'scheme': 'bearer', 'bearerFormat': 'JWT'}
+
+    spec.components.security_scheme("api_key", **api_key_scheme)
+    spec.components.security_scheme("jwt", **jwt_scheme)
+
+    pprint(spec.to_dict()['components']['securitySchemes'], indent=2)
+    # { 'api_key': {'in': 'header', 'name': 'X-API-Key', 'type': 'apiKey'},
+    #   'jwt': {'bearerFormat': 'JWT', 'scheme': 'bearer', 'type': 'http'}}


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
index b1b9aec..8ef62ab 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -7,7 +7,7 @@ import yaml
 from apispec import APISpec, BasePlugin
 from apispec.exceptions import APISpecError, DuplicateComponentNameError
 
-from .utils import get_definitions, get_paths, get_parameters, get_responses
+from .utils import get_definitions, get_paths, get_parameters, get_responses, get_security_schemes
 
 
 description = 'This is a sample Petstore server.  You can find out more '
@@ -397,6 +397,27 @@ class TestPath:
         ):
             spec.components.response('test_response', **route_spec['responses']['200'])
 
+    def test_security_scheme(self, spec):
+        sec_scheme = {'type': 'apiKey', 'in': 'header', 'name': 'X-API-Key'}
+        spec.components.security_scheme('ApiKeyAuth', **sec_scheme)
+        assert get_security_schemes(spec)['ApiKeyAuth'] == sec_scheme
+
+    def test_security_scheme_is_chainable(self, spec):
+        spec.components.security_scheme('sec_1').security_scheme('sec_2')
+        security_schemes = get_security_schemes(spec)
+        assert 'sec_1' in security_schemes
+        assert 'sec_2' in security_schemes
+
+    def test_security_scheme_duplicate_name(self, spec):
+        sec_scheme_1 = {'type': 'apiKey', 'in': 'header', 'name': 'X-API-Key'}
+        sec_scheme_2 = {'type': 'apiKey', 'in': 'header', 'name': 'X-API-Key-2'}
+        spec.components.security_scheme('ApiKeyAuth', **sec_scheme_1)
+        with pytest.raises(
+            DuplicateComponentNameError,
+            match='Another security scheme with name "ApiKeyAuth" is already registered.',
+        ):
+            spec.components.security_scheme('ApiKeyAuth', **sec_scheme_2)
+
     def test_path_check_invalid_http_method(self, spec):
         spec.path('/pet/{petId}', operations={'get': {}})
         spec.path('/pet/{petId}', operations={'x-dummy': {}})
diff --git a/tests/utils.py b/tests/utils.py
index 1a56bcf..cead926 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -16,6 +16,11 @@ def get_responses(spec):
         return spec.to_dict()['responses']
     return spec.to_dict()['components']['responses']
 
+def get_security_schemes(spec):
+    if spec.openapi_version.major < 3:
+        return spec.to_dict()['securityDefinitions']
+    return spec.to_dict()['components']['securitySchemes']
+
 def get_paths(spec):
     return spec.to_dict()['paths']