Skip to content

Commit 1a30368

Browse files
authored
Merge pull request #66 from uclahs-cds/aholmes-middleware-adjustments
Alter how CORS is handled in BL_Python.
2 parents 85ddfb0 + 51d8590 commit 1a30368

File tree

12 files changed

+71
-49
lines changed

12 files changed

+71
-49
lines changed

CHANGELOG.md

+9-6
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,20 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
77

88
---
99

10-
## [Unreleased]
10+
# `BL_Python.all` [0.2.4] - 2024-05-17
1111

12-
# `BL_Python.web` [0.2.2] - 2024-05-16
13-
- [BL_Python.web v0.2.2](https://github.com/uclahs-cds/BL_Python/blob/BL_Python.web-v0.2.2/src/web/CHANGELOG.md#022---2024-05-16)
14-
15-
# `BL_Python.platform` [0.2.2] - 2024-05-16
16-
- [BL_Python.platform v0.2.2](https://github.com/uclahs-cds/BL_Python/blob/BL_Python.platform-v0.2.2/src/platform/CHANGELOG.md#022---2024-05-16)
12+
## `BL_Python.web` [0.2.3] - 2024-05-17
13+
- [BL_Python.web v0.2.3](https://github.com/uclahs-cds/BL_Python/blob/BL_Python.web-v0.2.3/src/web/CHANGELOG.md#023---2024-05-17)
1714

1815
# `BL_Python.all` [0.2.3] - 2024-05-16
1916
- Contains all libraries up to v0.2.1, and `BL_Python.platform` v0.2.2 and `BL_Python.web` v0.2.2
2017

18+
## `BL_Python.web` [0.2.2] - 2024-05-16
19+
- [BL_Python.web v0.2.2](https://github.com/uclahs-cds/BL_Python/blob/BL_Python.web-v0.2.2/src/web/CHANGELOG.md#022---2024-05-16)
20+
21+
## `BL_Python.platform` [0.2.2] - 2024-05-16
22+
- [BL_Python.platform v0.2.2](https://github.com/uclahs-cds/BL_Python/blob/BL_Python.platform-v0.2.2/src/platform/CHANGELOG.md#022---2024-05-16)
23+
2124
# [0.2.0] - 2024-05-14
2225
### Added
2326
- New package `BL_Python.identity` for SSO

src/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__: str = "0.2.3"
1+
__version__: str = "0.2.4"

src/web/BL_Python/web/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__: str = "0.2.2"
1+
__version__: str = "0.2.3"

src/web/BL_Python/web/config.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ class LoggingConfig(BaseModel):
1212

1313

1414
class WebSecurityCorsConfig(BaseModel):
15-
origin: str | None = None
16-
allow_credentials: bool = True
15+
origins: list[str] | None = None
16+
allow_credentials: bool = False
1717
allow_methods: list[
1818
Literal[
1919
"GET",
@@ -27,6 +27,7 @@ class WebSecurityCorsConfig(BaseModel):
2727
"TRACE",
2828
]
2929
] = field(default_factory=lambda: ["GET", "POST", "OPTIONS"])
30+
allow_headers: list[str | Literal["*"]] = field(default_factory=lambda: ["*"])
3031

3132

3233
class WebSecurityConfig(BaseModel):

src/web/BL_Python/web/middleware/dependency_injection.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ def configure_dependencies(
8787
module, "register_middleware", None
8888
)
8989
if register_callback is not None and callable(register_callback):
90-
register_callback(app)
90+
flask_injector.injector.call_with_injection(
91+
register_callback, kwargs={"app": app}
92+
)
9193

9294
# this binds all BL_Python middlewares with Injector
9395
_configure_openapi_middleware_dependencies(app, flask_injector)

src/web/BL_Python/web/middleware/flask/__init__.py

+21-9
Original file line numberDiff line numberDiff line change
@@ -161,17 +161,29 @@ def _ordered_api_response_handers(response: Response, config: Config, log: Logge
161161
def _wrap_all_api_responses(response: Response, config: Config, log: Logger):
162162
correlation_id = _get_correlation_id(log)
163163

164-
cors_domain: str | None = None
165-
if config.web.security.cors.origin:
166-
cors_domain = config.web.security.cors.origin
164+
cors_domains: list[str] | None = None
165+
if config.web.security.cors.origins:
166+
cors_domains = config.web.security.cors.origins
167167
else:
168168
if not response.headers.get(CORS_ACCESS_CONTROL_ALLOW_ORIGIN_HEADER):
169-
cors_domain = request.headers.get(ORIGIN_HEADER)
170-
if not cors_domain:
171-
cors_domain = request.headers.get(HOST_HEADER)
172-
173-
if cors_domain:
174-
response.headers[CORS_ACCESS_CONTROL_ALLOW_ORIGIN_HEADER] = cors_domain
169+
cors_domains = (
170+
(header_value := request.headers.get(ORIGIN_HEADER))
171+
and [header_value]
172+
or None
173+
)
174+
if not cors_domains:
175+
cors_domains = (
176+
(header_value := request.headers.get(HOST_HEADER))
177+
and [header_value]
178+
or None
179+
)
180+
181+
if cors_domains:
182+
# although the config allows multiple, the header may only contain one.
183+
# either only one will have been set, or we take the first from the config.
184+
# this may not be valid in all cases, and so the ASGI CORSMiddleware sould
185+
# be used instead
186+
response.headers[CORS_ACCESS_CONTROL_ALLOW_ORIGIN_HEADER] = cors_domains[0]
175187

176188
response.headers[CORS_ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER] = str(
177189
config.web.security.cors.allow_credentials

src/web/BL_Python/web/middleware/openapi/__init__.py

-26
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,7 @@
2727
from ..consts import (
2828
CONTENT_SECURITY_POLICY_HEADER,
2929
CORRELATION_ID_HEADER,
30-
CORS_ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER,
31-
CORS_ACCESS_CONTROL_ALLOW_METHODS_HEADER,
32-
CORS_ACCESS_CONTROL_ALLOW_ORIGIN_HEADER,
33-
HOST_HEADER,
3430
INCOMING_REQUEST_MESSAGE,
35-
ORIGIN_HEADER,
3631
OUTGOING_RESPONSE_MESSAGE,
3732
REQUEST_COOKIE_HEADER,
3833
RESPONSE_COOKIE_HEADER,
@@ -234,27 +229,6 @@ def _wrap_all_api_responses(
234229
correlation_id = _get_correlation_id(request, response, log)
235230
response_headers = _headers_as_dict(response)
236231

237-
cors_domain: str | None = None
238-
if config.web.security.cors.origin:
239-
cors_domain = config.web.security.cors.origin
240-
else:
241-
if not response_headers.get(CORS_ACCESS_CONTROL_ALLOW_ORIGIN_HEADER):
242-
request_headers = _headers_as_dict(request)
243-
cors_domain = request_headers.get(ORIGIN_HEADER)
244-
if not cors_domain:
245-
cors_domain = request_headers.get(HOST_HEADER)
246-
247-
if cors_domain:
248-
response_headers[CORS_ACCESS_CONTROL_ALLOW_ORIGIN_HEADER] = cors_domain
249-
250-
response_headers[CORS_ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER] = str(
251-
config.web.security.cors.allow_credentials
252-
)
253-
254-
response_headers[CORS_ACCESS_CONTROL_ALLOW_METHODS_HEADER] = ",".join(
255-
config.web.security.cors.allow_methods
256-
)
257-
258232
response_headers[CORRELATION_ID_HEADER] = correlation_id
259233

260234
if config.web.security.csp:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from BL_Python.web.config import Config
2+
from connexion import FlaskApp
3+
from connexion.middleware import MiddlewarePosition
4+
from injector import Module, inject
5+
from starlette.middleware.cors import CORSMiddleware
6+
7+
8+
class CORSMiddlewareModule(Module):
9+
@inject
10+
def register_middleware(self, app: FlaskApp, config: Config):
11+
cors_config = config.web.security.cors
12+
13+
app.add_middleware(
14+
CORSMiddleware,
15+
position=MiddlewarePosition.BEFORE_EXCEPTION,
16+
allow_origins=cors_config.origins,
17+
allow_credentials=cors_config.allow_credentials,
18+
allow_methods=cors_config.allow_methods,
19+
allow_headers=cors_config.allow_headers,
20+
)

src/web/BL_Python/web/scaffolding/templates/basic/{{application.module_name}}/config.toml.j2

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ log_level = 'INFO'
2727
format = 'JSON'
2828

2929
[web.security.cors]
30-
origin = 'http://localhost:5000'
30+
origins = ['http://localhost:5000']
3131
allow_credentials = true
3232
allow_methods = ['GET', 'POST', 'PATCH', 'OPTIONS']
3333

src/web/BL_Python/web/scaffolding/templates/openapi/{{application.module_name}}/config.toml.j2

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ log_level = 'INFO'
3434
format = 'plaintext'
3535

3636
[web.security.cors]
37-
origin = 'http://localhost:5000'
37+
origins = ['http://localhost:5000']
3838
allow_credentials = true
3939
allow_methods = ['GET', 'POST', 'PATCH', 'OPTIONS']
4040

src/web/CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
99
Review the `BL_Python` [CHANGELOG.md](https://github.com/uclahs-cds/BL_Python/blob/main/CHANGELOG.md) for full monorepo notes.
1010

1111
---
12+
## Unreleased
13+
14+
## [0.2.3] - 2024-05-17
15+
### Changed
16+
- Flask and OpenAPI apps require configuration of CORS origins as an array
17+
- Flask apps still only supports a single origin
18+
- OpenAPI apps use `CORSMiddleware` in favor of the custom CORS handlers
19+
20+
### Added
21+
- Custom middleware modules can now use Injector to get interfaces that have been previously registered
1222

1323
## [0.2.2] - 2024-05-16
1424
### Changed

src/web/test/unit/middleware/test_api_response_handlers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def test__wrap_all_api_responses__sets_CSP_header(
4949
@pytest.mark.parametrize(
5050
"header,value,config_attribute_name",
5151
[
52-
(CORS_ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, "example.com", "origin"),
52+
(CORS_ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, ["example.com"], "origins"),
5353
(
5454
CORS_ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER,
5555
"False",

0 commit comments

Comments
 (0)