feat: allow to configure cors

This commit is contained in:
2026-03-23 16:39:07 +02:00
parent 3ad2c494af
commit 40671b99d5
6 changed files with 94 additions and 7 deletions

View File

@@ -180,6 +180,10 @@ Web server settings. This feature requires ``aiohttp`` libraries to be installed
* ``address`` - optional address in form ``proto://host:port`` (``port`` can be omitted in case of default ``proto`` ports), will be used instead of ``http://{host}:{port}`` in case if set, string, optional. This option is required in case if ``OAuth`` provider is used.
* ``autorefresh_intervals`` - enable page auto refresh options, space separated list of integers, optional. The first defined interval will be used as default. If no intervals set, the auto refresh buttons will be disabled. If first element of the list equals ``0``, auto refresh will be disabled by default.
* ``cors_allow_headers`` - allowed CORS headers, space separated list of strings, optional.
* ``cors_allow_methods`` - allowed CORS methods, space separated list of strings, optional.
* ``cors_allow_origins`` - allowed CORS origins, space separated list of strings, optional, default ``*``.
* ``cors_expose_headers`` - exposed CORS headers, space separated list of strings, optional.
* ``enable_archive_upload`` - allow to upload packages via HTTP (i.e. call of ``/api/v1/service/upload`` uri), boolean, optional, default ``no``.
* ``host`` - host to bind, string, optional.
* ``index_url`` - full URL of the repository index page, string, optional.

View File

@@ -30,6 +30,14 @@ allow_read_only = yes
; If no intervals set, auto refresh will be disabled. 0 can only be the first element and will disable auto refresh
; by default.
autorefresh_intervals = 5 1 10 30 60
; Allowed CORS headers. By default everything is allowed.
;cors_allow_headers =
; Allowed CORS methods. By default everything is allowed.
;cors_allow_methods =
; Allowed CORS origins.
;cors_allow_origins = *
; Exposed CORS headers. By default everything is exposed.
;cors_expose_headers =
; Enable file upload endpoint used by some triggers.
;enable_archive_upload = no
; Address to bind the server.

View File

@@ -358,6 +358,38 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"min": 0,
},
},
"cors_allow_headers": {
"type": "list",
"coerce": "list",
"schema": {
"type": "string",
"empty": False,
},
},
"cors_allow_methods": {
"type": "list",
"coerce": "list",
"schema": {
"type": "string",
"empty": False,
},
},
"cors_allow_origins": {
"type": "list",
"coerce": "list",
"schema": {
"type": "string",
"empty": False,
},
},
"cors_expose_headers": {
"type": "list",
"coerce": "list",
"schema": {
"type": "string",
"empty": False,
},
},
"enable_archive_upload": {
"type": "boolean",
"coerce": "boolean",

View File

@@ -21,26 +21,34 @@ import aiohttp_cors
from aiohttp.web import Application
from ahriman.core.configuration import Configuration
__all__ = ["setup_cors"]
def setup_cors(application: Application) -> aiohttp_cors.CorsConfig:
def setup_cors(application: Application, configuration: Configuration) -> aiohttp_cors.CorsConfig:
"""
setup CORS for the web application
Args:
application(Application): web application instance
configuration(Configuration): configuration instance
Returns:
aiohttp_cors.CorsConfig: generated CORS configuration
"""
allow_headers = configuration.getlist("web", "cors_allow_headers", fallback=[]) or "*"
allow_methods = configuration.getlist("web", "cors_allow_methods", fallback=[]) or "*"
expose_headers = configuration.getlist("web", "cors_expose_headers", fallback=[]) or "*"
cors = aiohttp_cors.setup(application, defaults={
"*": aiohttp_cors.ResourceOptions( # type: ignore[no-untyped-call]
expose_headers="*",
allow_headers="*",
allow_methods="*",
origin: aiohttp_cors.ResourceOptions( # type: ignore[no-untyped-call]
expose_headers=expose_headers,
allow_headers=allow_headers,
allow_methods=allow_methods,
)
for origin in configuration.getlist("web", "cors_allow_origins", fallback=["*"])
})
for route in application.router.routes():
cors.add(route)

View File

@@ -187,7 +187,7 @@ def setup_server(configuration: Configuration, spawner: Spawn, repositories: lis
setup_routes(application, configuration)
application.logger.info("setup CORS")
setup_cors(application)
setup_cors(application, configuration)
application.logger.info("setup templates")
loader = jinja2.FileSystemLoader(searchpath=configuration.getpathlist("web", "templates"))

View File

@@ -2,13 +2,17 @@ import aiohttp_cors
import pytest
from aiohttp.web import Application
from pytest_mock import MockerFixture
from ahriman.web.cors import setup_cors
from ahriman.web.keys import ConfigurationKey
def test_setup_cors(application: Application) -> None:
"""
must setup CORS
"""
cors: aiohttp_cors.CorsConfig = application[aiohttp_cors.APP_CONFIG_KEY]
cors = application[aiohttp_cors.APP_CONFIG_KEY]
# let's test here that it is enabled for all requests
for route in application.router.routes():
# we don't want to deal with match info here though
@@ -18,3 +22,34 @@ def test_setup_cors(application: Application) -> None:
continue
request = pytest.helpers.request(application, url, route.method, resource=route.resource)
assert cors._cors_impl._router_adapter.is_cors_enabled_on_request(request)
def test_setup_cors_custom_origins(application: Application, mocker: MockerFixture) -> None:
"""
must setup CORS with custom origins
"""
configuration = application[ConfigurationKey]
configuration.set_option("web", "cors_allow_origins", "https://example.com https://httpbin.com")
setup_mock = mocker.patch("ahriman.web.cors.aiohttp_cors.setup", return_value=mocker.MagicMock())
setup_cors(application, configuration)
defaults = setup_mock.call_args.kwargs["defaults"]
assert "https://example.com" in defaults
assert "https://httpbin.com" in defaults
assert "*" not in defaults
def test_setup_cors_custom_methods(application: Application, mocker: MockerFixture) -> None:
"""
must setup CORS with custom methods
"""
configuration = application[ConfigurationKey]
configuration.set_option("web", "cors_allow_methods", "GET POST")
setup_mock = mocker.patch("ahriman.web.cors.aiohttp_cors.setup", return_value=mocker.MagicMock())
setup_cors(application, configuration)
defaults = setup_mock.call_args.kwargs["defaults"]
resource_options = next(iter(defaults.values()))
assert resource_options.allow_methods == {"GET", "POST"}