diff --git a/connexion/apps/abstract.py b/connexion/apps/abstract.py index 75daa1cc2..222934f5e 100644 --- a/connexion/apps/abstract.py +++ b/connexion/apps/abstract.py @@ -51,6 +51,7 @@ def __init__( validate_responses: t.Optional[bool] = None, validator_map: t.Optional[dict] = None, security_map: t.Optional[dict] = None, + no_security: t.Optional[bool] = None, ) -> None: """ :param import_name: The name of the package or module that this object belongs to. If you @@ -84,6 +85,9 @@ def __init__( :obj:`validators.VALIDATOR_MAP`. :param security_map: A dictionary of security handlers to use. Defaults to :obj:`security.SECURITY_HANDLERS` + :param no_security: Disable security verification. Useful for prototyping + or if security is handled by an API Gateway in front of your application. Defaults to + False. """ self.middleware = ConnexionMiddleware( self._middleware_app, @@ -103,6 +107,7 @@ def __init__( validate_responses=validate_responses, validator_map=validator_map, security_map=security_map, + no_security=no_security, ) def add_middleware( @@ -137,6 +142,7 @@ def add_api( validate_responses: t.Optional[bool] = None, validator_map: t.Optional[dict] = None, security_map: t.Optional[dict] = None, + no_security: t.Optional[bool] = None, **kwargs, ) -> t.Any: """ @@ -171,6 +177,9 @@ def add_api( :obj:`validators.VALIDATOR_MAP` :param security_map: A dictionary of security handlers to use. Defaults to :obj:`security.SECURITY_HANDLERS` + :param no_security: Disable security verification. Useful for prototyping + or if security is handled by an API Gateway in front of your application. Defaults to + False. :param kwargs: Additional keyword arguments to pass to the `add_api` method of the managed middlewares. This can be used to pass arguments to middlewares added beyond the default ones. @@ -193,6 +202,7 @@ def add_api( validate_responses=validate_responses, validator_map=validator_map, security_map=security_map, + no_security=no_security, **kwargs, ) diff --git a/connexion/apps/asynchronous.py b/connexion/apps/asynchronous.py index 9ab08a401..969685d40 100644 --- a/connexion/apps/asynchronous.py +++ b/connexion/apps/asynchronous.py @@ -143,6 +143,7 @@ def __init__( validate_responses: t.Optional[bool] = None, validator_map: t.Optional[dict] = None, security_map: t.Optional[dict] = None, + no_security: t.Optional[bool] = None, ) -> None: """ :param import_name: The name of the package or module that this object belongs to. If you @@ -177,6 +178,9 @@ def __init__( :obj:`validators.VALIDATOR_MAP`. :param security_map: A dictionary of security handlers to use. Defaults to :obj:`security.SECURITY_HANDLERS` + :param no_security: Disable security verification. Useful for prototyping + or if security is handled by an API Gateway in front of your application. Defaults to + False. """ self._middleware_app: AsyncASGIApp = AsyncASGIApp() @@ -197,6 +201,7 @@ def __init__( validate_responses=validate_responses, validator_map=validator_map, security_map=security_map, + no_security=no_security, ) def add_url_rule( diff --git a/connexion/apps/flask.py b/connexion/apps/flask.py index 057ce8baa..11b4aa898 100644 --- a/connexion/apps/flask.py +++ b/connexion/apps/flask.py @@ -176,6 +176,7 @@ def __init__( validate_responses: t.Optional[bool] = None, validator_map: t.Optional[dict] = None, security_map: t.Optional[dict] = None, + no_security: t.Optional[bool] = None, ): """ :param import_name: The name of the package or module that this object belongs to. If you @@ -213,6 +214,9 @@ def __init__( :obj:`validators.VALIDATOR_MAP`. :param security_map: A dictionary of security handlers to use. Defaults to :obj:`security.SECURITY_HANDLERS` + :param no_security: Disable security verification. Useful for prototyping + or if security is handled by an API Gateway in front of your application. Defaults to + False. """ self._middleware_app = FlaskASGIApp(import_name, server_args or {}) @@ -233,6 +237,7 @@ def __init__( validate_responses=validate_responses, validator_map=validator_map, security_map=security_map, + no_security=no_security, ) self.app = self._middleware_app.app diff --git a/connexion/cli.py b/connexion/cli.py index 73fd2726b..a9aeeea8a 100644 --- a/connexion/cli.py +++ b/connexion/cli.py @@ -93,6 +93,11 @@ def run(app: AbstractApp, args: argparse.Namespace): action="count", default=0, ) +run_parser.add_argument( + "--no-security", + help="Disable security checks.", + action="store_true", +) run_parser.add_argument("--base-path", help="Override the basePath in the API spec.") run_parser.add_argument( "--app-framework", @@ -126,10 +131,13 @@ def create_app(args: t.Optional[argparse.Namespace] = None) -> AbstractApp: if args.stub: resolver_error = 501 - api_extra_args = {} + api_extra_args: t.Dict[str, t.Any] = {} if args.mock: resolver = MockResolver(mock_all=args.mock == "all") api_extra_args["resolver"] = resolver + if args.no_security: + logger.warning("Disabling security checks on the API.") + api_extra_args["no_security"] = True app_cls = connexion.utils.get_function_from_name(AVAILABLE_APPS[args.app_framework]) diff --git a/connexion/middleware/main.py b/connexion/middleware/main.py index 641576634..a271ddfc5 100644 --- a/connexion/middleware/main.py +++ b/connexion/middleware/main.py @@ -61,6 +61,7 @@ class _Options: validate_responses: t.Optional[bool] = False validator_map: t.Optional[dict] = None security_map: t.Optional[dict] = None + no_security: t.Optional[bool] = False def __post_init__(self): self.resolver = ( @@ -212,6 +213,7 @@ def __init__( validate_responses: t.Optional[bool] = None, validator_map: t.Optional[dict] = None, security_map: t.Optional[dict] = None, + no_security: t.Optional[bool] = None, ): """ :param import_name: The name of the package or module that this object belongs to. If you @@ -244,6 +246,9 @@ def __init__( :obj:`validators.VALIDATOR_MAP`. :param security_map: A dictionary of security handlers to use. Defaults to :obj:`security.SECURITY_HANDLERS`. + :param no_security: Disable security verification. Useful for prototyping + or if security is handled by an API Gateway in front of your application. Defaults to + False. """ import_name = import_name or str(pathlib.Path.cwd()) self.root_path = utils.get_root_path(import_name) @@ -277,6 +282,7 @@ def __init__( validate_responses=validate_responses, validator_map=validator_map, security_map=security_map, + no_security=no_security, ) self.extra_files: t.List[str] = [] @@ -365,6 +371,7 @@ def add_api( validate_responses: t.Optional[bool] = None, validator_map: t.Optional[dict] = None, security_map: t.Optional[dict] = None, + no_security: t.Optional[bool] = False, **kwargs, ) -> None: """ @@ -399,6 +406,9 @@ def add_api( :obj:`validators.VALIDATOR_MAP` :param security_map: A dictionary of security handlers to use. Defaults to :obj:`security.SECURITY_HANDLERS` + :param no_security: Disable security verification. Useful for prototyping + or if security is handled by an API Gateway in front of your application. Defaults to + False. :param kwargs: Additional keyword arguments to pass to the `add_api` method of the managed middlewares. This can be used to pass arguments to middlewares added beyond the default ones. @@ -431,6 +441,7 @@ def add_api( validate_responses=validate_responses, validator_map=validator_map, security_map=security_map, + no_security=no_security, ) api = API( diff --git a/connexion/middleware/security.py b/connexion/middleware/security.py index f6d150fcf..9240f4b95 100644 --- a/connexion/middleware/security.py +++ b/connexion/middleware/security.py @@ -36,6 +36,7 @@ def from_operation( *, next_app: ASGIApp, security_handler_factory: SecurityHandlerFactory, + no_security: bool = False, ) -> "SecurityOperation": """Create a SecurityOperation from an Operation of Specification instance @@ -47,10 +48,15 @@ def from_operation( :param security_handler_factory: The factory to be used to generate security handlers for the different security schemes. """ + if no_security: + security = [] + else: + security = operation.security + return cls( next_app=next_app, security_handler_factory=security_handler_factory, - security=operation.security, + security=security, security_schemes=operation.security_schemes, ) @@ -113,11 +119,17 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: class SecurityAPI(RoutedAPI[SecurityOperation]): def __init__( - self, *args, auth_all_paths: bool = False, security_map: dict = None, **kwargs + self, + *args, + auth_all_paths: bool = False, + security_map: dict = None, + no_security: bool = False, + **kwargs, ): super().__init__(*args, **kwargs) self.security_handler_factory = SecurityHandlerFactory(security_map) + self.no_security = no_security if auth_all_paths: self.add_auth_on_not_found() @@ -145,6 +157,7 @@ def make_operation( operation, next_app=self.next_app, security_handler_factory=self.security_handler_factory, + no_security=self.no_security, ) diff --git a/tests/api/conftest.py b/tests/api/conftest.py index 9249e6126..12a0ff93a 100644 --- a/tests/api/conftest.py +++ b/tests/api/conftest.py @@ -146,6 +146,17 @@ def secure_endpoint_app(spec, app_class): ) +@pytest.fixture(scope="session") +def secure_endpoint_app_no_security(spec, app_class): + return build_app_from_fixture( + "secure_endpoint", + app_class=app_class, + spec_file=spec, + validate_responses=True, + no_security=True, + ) + + @pytest.fixture(scope="session") def secure_endpoint_strict_app(spec, app_class): return build_app_from_fixture( diff --git a/tests/api/test_secure_api.py b/tests/api/test_secure_api.py index cf547965f..282e441cf 100644 --- a/tests/api/test_secure_api.py +++ b/tests/api/test_secure_api.py @@ -170,6 +170,16 @@ def test_security(oauth_requests, secure_endpoint_app): assert response.status_code == 401 +def test_disabled_security(secure_endpoint_app_no_security): + # Test that disabling security allows unauthenticated access to an otherwise + # secure endpoint. + app_client = secure_endpoint_app_no_security.test_client() + + get_bye_no_auth = app_client.get("/v1.0/byesecure-ignoring-context/jsantos") + assert get_bye_no_auth.status_code == 200 + assert get_bye_no_auth.text == "Goodbye jsantos (Secure!)" + + def test_checking_that_client_token_has_all_necessary_scopes( oauth_requests, secure_endpoint_app ):