mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-24 15:27:17 +00:00
feat: add ability to disable specific routes (#119)
This commit is contained in:
parent
c54b14b833
commit
e784032bc6
@ -129,6 +129,7 @@ Web server settings. If any of ``host``/``port`` is not set, web integration wil
|
|||||||
* ``index_url`` - full url of the repository index page, string, optional.
|
* ``index_url`` - full url of the repository index page, string, optional.
|
||||||
* ``max_body_size`` - max body size in bytes to be validated for archive upload, integer, optional. If not set, validation will be disabled.
|
* ``max_body_size`` - max body size in bytes to be validated for archive upload, integer, optional. If not set, validation will be disabled.
|
||||||
* ``port`` - port to bind, integer, optional.
|
* ``port`` - port to bind, integer, optional.
|
||||||
|
* ``service_only`` - disable status routes (including logs), boolean, optional, default ``no``.
|
||||||
* ``static_path`` - path to directory with static files, string, required.
|
* ``static_path`` - path to directory with static files, string, required.
|
||||||
* ``templates`` - path to templates directories, space separated list of strings, required.
|
* ``templates`` - path to templates directories, space separated list of strings, required.
|
||||||
* ``unix_socket`` - path to the listening unix socket, string, optional. If set, server will create the socket on the specified address which can (and will) be used by application. Note, that unlike usual host/port configuration, unix socket allows to perform requests without authorization.
|
* ``unix_socket`` - path to the listening unix socket, string, optional. If set, server will create the socket on the specified address which can (and will) be used by application. Note, that unlike usual host/port configuration, unix socket allows to perform requests without authorization.
|
||||||
|
@ -1052,7 +1052,7 @@ It is required to point to the master node repository, otherwise internal depend
|
|||||||
|
|
||||||
Also, in case if authentication is enabled, the same user with the same password must be created for all workers.
|
Also, in case if authentication is enabled, the same user with the same password must be created for all workers.
|
||||||
|
|
||||||
It is also recommended to set ``web.wait_timeout`` to infinte in case of multiple conflicting runs.
|
It is also recommended to set ``web.wait_timeout`` to infinite in case of multiple conflicting runs and ``service_only`` to ``yes`` in order to disable status endpoints.
|
||||||
|
|
||||||
Other settings are the same as mentioned above.
|
Other settings are the same as mentioned above.
|
||||||
|
|
||||||
@ -1107,6 +1107,9 @@ Worker nodes (applicable for all workers) config (``worker.ini``) as:
|
|||||||
manual = yes
|
manual = yes
|
||||||
wait_timeout = 0
|
wait_timeout = 0
|
||||||
|
|
||||||
|
[web]
|
||||||
|
service_only = yes
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
triggers = ahriman.core.upload.UploadTrigger ahriman.core.report.ReportTrigger
|
triggers = ahriman.core.upload.UploadTrigger ahriman.core.report.ReportTrigger
|
||||||
|
|
||||||
|
@ -342,6 +342,10 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
|||||||
"min": 0,
|
"min": 0,
|
||||||
"max": 65535,
|
"max": 65535,
|
||||||
},
|
},
|
||||||
|
"service_only": {
|
||||||
|
"type": "boolean",
|
||||||
|
"coerce": "boolean",
|
||||||
|
},
|
||||||
"static_path": {
|
"static_path": {
|
||||||
"type": "path",
|
"type": "path",
|
||||||
"coerce": "absolute_path",
|
"coerce": "absolute_path",
|
||||||
|
@ -25,18 +25,20 @@ from pkgutil import ModuleInfo, iter_modules
|
|||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Any, Type, TypeGuard
|
from typing import Any, Type, TypeGuard
|
||||||
|
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["setup_routes"]
|
__all__ = ["setup_routes"]
|
||||||
|
|
||||||
|
|
||||||
def _dynamic_routes(module_root: Path) -> dict[str, Type[View]]:
|
def _dynamic_routes(module_root: Path, configuration: Configuration) -> dict[str, Type[View]]:
|
||||||
"""
|
"""
|
||||||
extract dynamic routes based on views
|
extract dynamic routes based on views
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
module_root(Path): root module path with views
|
module_root(Path): root module path with views
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict[str, Type[View]]: map of the route to its view
|
dict[str, Type[View]]: map of the route to its view
|
||||||
@ -52,7 +54,9 @@ def _dynamic_routes(module_root: Path) -> dict[str, Type[View]]:
|
|||||||
view = getattr(module, attribute_name)
|
view = getattr(module, attribute_name)
|
||||||
if not is_base_view(view):
|
if not is_base_view(view):
|
||||||
continue
|
continue
|
||||||
routes.update([(route, view) for route in view.ROUTES])
|
|
||||||
|
view_routes = view.routes(configuration)
|
||||||
|
routes.update([(route, view) for route in view_routes])
|
||||||
|
|
||||||
return routes
|
return routes
|
||||||
|
|
||||||
@ -101,16 +105,16 @@ def _modules(module_root: Path) -> Generator[ModuleInfo, None, None]:
|
|||||||
yield module_info
|
yield module_info
|
||||||
|
|
||||||
|
|
||||||
def setup_routes(application: Application, static_path: Path) -> None:
|
def setup_routes(application: Application, configuration: Configuration) -> None:
|
||||||
"""
|
"""
|
||||||
setup all defined routes
|
setup all defined routes
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
application(Application): web application instance
|
application(Application): web application instance
|
||||||
static_path(Path): path to static files directory
|
configuration(Configuration): configuration instance
|
||||||
"""
|
"""
|
||||||
application.router.add_static("/static", static_path, follow_symlinks=True)
|
application.router.add_static("/static", configuration.getpath("web", "static_path"), follow_symlinks=True)
|
||||||
|
|
||||||
views = Path(__file__).parent / "views"
|
views_root = Path(__file__).parent / "views"
|
||||||
for route, view in _dynamic_routes(views).items():
|
for route, view in _dynamic_routes(views_root, configuration).items():
|
||||||
application.router.add_view(route, view)
|
application.router.add_view(route, view)
|
||||||
|
@ -115,6 +115,21 @@ class BaseView(View, CorsViewMixin):
|
|||||||
permission: UserAccess = getattr(cls, f"{method}_PERMISSION", UserAccess.Full)
|
permission: UserAccess = getattr(cls, f"{method}_PERMISSION", UserAccess.Full)
|
||||||
return permission
|
return permission
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def routes(cls, configuration: Configuration) -> list[str]:
|
||||||
|
"""
|
||||||
|
extract routes list for the view
|
||||||
|
|
||||||
|
Args:
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[str]: list of routes defined for the view. By default, it tries to read :attr:`ROUTES` option if set
|
||||||
|
and returns empty list otherwise
|
||||||
|
"""
|
||||||
|
del configuration
|
||||||
|
return cls.ROUTES
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_non_empty(extractor: Callable[[str], T | None], key: str) -> T:
|
def get_non_empty(extractor: Callable[[str], T | None], key: str) -> T:
|
||||||
"""
|
"""
|
||||||
|
41
src/ahriman/web/views/status_view_guard.py
Normal file
41
src/ahriman/web/views/status_view_guard.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
|
||||||
|
|
||||||
|
class StatusViewGuard:
|
||||||
|
|
||||||
|
ROUTES: list[str]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def routes(cls, configuration: Configuration) -> list[str]:
|
||||||
|
"""
|
||||||
|
extract routes list for the view
|
||||||
|
|
||||||
|
Args:
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[str]: list of routes defined for the view. By default, it tries to read :attr:`ROUTES` option if set
|
||||||
|
and returns empty list otherwise
|
||||||
|
"""
|
||||||
|
if configuration.getboolean("web", "service_only", fallback=False):
|
||||||
|
return []
|
||||||
|
return cls.ROUTES
|
@ -26,9 +26,10 @@ from ahriman.models.changes import Changes
|
|||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.schemas import AuthSchema, ChangesSchema, ErrorSchema, PackageNameSchema, RepositoryIdSchema
|
from ahriman.web.schemas import AuthSchema, ChangesSchema, ErrorSchema, PackageNameSchema, RepositoryIdSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
from ahriman.web.views.status_view_guard import StatusViewGuard
|
||||||
|
|
||||||
|
|
||||||
class ChangesView(BaseView):
|
class ChangesView(StatusViewGuard, BaseView):
|
||||||
"""
|
"""
|
||||||
package changes web view
|
package changes web view
|
||||||
|
|
||||||
|
@ -28,9 +28,10 @@ from ahriman.models.user_access import UserAccess
|
|||||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, LogsSchema, PackageNameSchema, RepositoryIdSchema, \
|
from ahriman.web.schemas import AuthSchema, ErrorSchema, LogsSchema, PackageNameSchema, RepositoryIdSchema, \
|
||||||
VersionedLogSchema
|
VersionedLogSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
from ahriman.web.views.status_view_guard import StatusViewGuard
|
||||||
|
|
||||||
|
|
||||||
class LogsView(BaseView):
|
class LogsView(StatusViewGuard, BaseView):
|
||||||
"""
|
"""
|
||||||
package logs web view
|
package logs web view
|
||||||
|
|
||||||
|
@ -28,9 +28,10 @@ from ahriman.models.user_access import UserAccess
|
|||||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNameSchema, PackageStatusSchema, \
|
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNameSchema, PackageStatusSchema, \
|
||||||
PackageStatusSimplifiedSchema, RepositoryIdSchema
|
PackageStatusSimplifiedSchema, RepositoryIdSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
from ahriman.web.views.status_view_guard import StatusViewGuard
|
||||||
|
|
||||||
|
|
||||||
class PackageView(BaseView):
|
class PackageView(StatusViewGuard, BaseView):
|
||||||
"""
|
"""
|
||||||
package base specific web view
|
package base specific web view
|
||||||
|
|
||||||
|
@ -28,9 +28,10 @@ from ahriman.models.package import Package
|
|||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageStatusSchema, PaginationSchema, RepositoryIdSchema
|
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageStatusSchema, PaginationSchema, RepositoryIdSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
from ahriman.web.views.status_view_guard import StatusViewGuard
|
||||||
|
|
||||||
|
|
||||||
class PackagesView(BaseView):
|
class PackagesView(StatusViewGuard, BaseView):
|
||||||
"""
|
"""
|
||||||
global watcher view
|
global watcher view
|
||||||
|
|
||||||
|
@ -24,9 +24,10 @@ from aiohttp.web import HTTPNoContent, HTTPNotFound, Response, json_response
|
|||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, PatchNameSchema, PatchSchema
|
from ahriman.web.schemas import AuthSchema, ErrorSchema, PatchNameSchema, PatchSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
from ahriman.web.views.status_view_guard import StatusViewGuard
|
||||||
|
|
||||||
|
|
||||||
class PatchView(BaseView):
|
class PatchView(StatusViewGuard, BaseView):
|
||||||
"""
|
"""
|
||||||
package patch web view
|
package patch web view
|
||||||
|
|
||||||
|
@ -25,9 +25,10 @@ from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
|||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNameSchema, PatchSchema
|
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNameSchema, PatchSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
from ahriman.web.views.status_view_guard import StatusViewGuard
|
||||||
|
|
||||||
|
|
||||||
class PatchesView(BaseView):
|
class PatchesView(StatusViewGuard, BaseView):
|
||||||
"""
|
"""
|
||||||
package patches web view
|
package patches web view
|
||||||
|
|
||||||
|
@ -28,9 +28,10 @@ from ahriman.models.internal_status import InternalStatus
|
|||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, InternalStatusSchema, StatusSchema, RepositoryIdSchema
|
from ahriman.web.schemas import AuthSchema, ErrorSchema, InternalStatusSchema, StatusSchema, RepositoryIdSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
from ahriman.web.views.status_view_guard import StatusViewGuard
|
||||||
|
|
||||||
|
|
||||||
class StatusView(BaseView):
|
class StatusView(StatusViewGuard, BaseView):
|
||||||
"""
|
"""
|
||||||
web service status web view
|
web service status web view
|
||||||
|
|
||||||
|
@ -24,9 +24,10 @@ from aiohttp.web import Response, json_response
|
|||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, LogSchema, PackageNameSchema, PaginationSchema
|
from ahriman.web.schemas import AuthSchema, ErrorSchema, LogSchema, PackageNameSchema, PaginationSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
from ahriman.web.views.status_view_guard import StatusViewGuard
|
||||||
|
|
||||||
|
|
||||||
class LogsView(BaseView):
|
class LogsView(StatusViewGuard, BaseView):
|
||||||
"""
|
"""
|
||||||
package logs web view
|
package logs web view
|
||||||
|
|
||||||
|
@ -146,7 +146,7 @@ def setup_server(configuration: Configuration, spawner: Spawn, repositories: lis
|
|||||||
application.middlewares.append(exception_handler(application.logger))
|
application.middlewares.append(exception_handler(application.logger))
|
||||||
|
|
||||||
application.logger.info("setup routes")
|
application.logger.info("setup routes")
|
||||||
setup_routes(application, configuration.getpath("web", "static_path"))
|
setup_routes(application, configuration)
|
||||||
|
|
||||||
application.logger.info("setup CORS")
|
application.logger.info("setup CORS")
|
||||||
setup_cors(application)
|
setup_cors(application)
|
||||||
|
@ -18,6 +18,8 @@ def test_test_coverage() -> None:
|
|||||||
elif (version := source_file.parts[4]) in ("v1", "v2"):
|
elif (version := source_file.parts[4]) in ("v1", "v2"):
|
||||||
api = source_file.parts[5]
|
api = source_file.parts[5]
|
||||||
filename = f"test_view_{version}_{api}_{source_file.name}"
|
filename = f"test_view_{version}_{api}_{source_file.name}"
|
||||||
|
elif source_file.name.endswith("_guard.py"):
|
||||||
|
filename = f"test_{source_file.name}"
|
||||||
else:
|
else:
|
||||||
filename = f"test_view_{source_file.name}"
|
filename = f"test_view_{source_file.name}"
|
||||||
else:
|
else:
|
||||||
|
@ -11,7 +11,7 @@ from ahriman.core.util import walk
|
|||||||
from ahriman.web.routes import _dynamic_routes, _module, _modules, setup_routes
|
from ahriman.web.routes import _dynamic_routes, _module, _modules, setup_routes
|
||||||
|
|
||||||
|
|
||||||
def test_dynamic_routes(resource_path_root: Path) -> None:
|
def test_dynamic_routes(resource_path_root: Path, configuration: Configuration) -> None:
|
||||||
"""
|
"""
|
||||||
must return all available routes
|
must return all available routes
|
||||||
"""
|
"""
|
||||||
@ -19,10 +19,10 @@ def test_dynamic_routes(resource_path_root: Path) -> None:
|
|||||||
expected_views = [
|
expected_views = [
|
||||||
file
|
file
|
||||||
for file in walk(views_root)
|
for file in walk(views_root)
|
||||||
if file.suffix == ".py" and file.name not in ("__init__.py", "base.py")
|
if file.suffix == ".py" and file.name not in ("__init__.py", "base.py", "status_view_guard.py")
|
||||||
]
|
]
|
||||||
|
|
||||||
routes = _dynamic_routes(views_root)
|
routes = _dynamic_routes(views_root, configuration)
|
||||||
assert all(isinstance(view, type) for view in routes.values())
|
assert all(isinstance(view, type) for view in routes.values())
|
||||||
assert len(set(routes.values())) == len(expected_views)
|
assert len(set(routes.values())) == len(expected_views)
|
||||||
|
|
||||||
@ -74,5 +74,5 @@ def test_setup_routes(application: Application, configuration: Configuration) ->
|
|||||||
"""
|
"""
|
||||||
must generate non-empty list of routes
|
must generate non-empty list of routes
|
||||||
"""
|
"""
|
||||||
setup_routes(application, configuration.getpath("web", "static_path"))
|
setup_routes(application, configuration)
|
||||||
assert application.router.routes()
|
assert application.router.routes()
|
||||||
|
19
tests/ahriman/web/views/test_status_view_guard.py
Normal file
19
tests/ahriman/web/views/test_status_view_guard.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.web.views.status_view_guard import StatusViewGuard
|
||||||
|
|
||||||
|
|
||||||
|
def test_routes(configuration: Configuration) -> None:
|
||||||
|
"""
|
||||||
|
must correctly return routes list
|
||||||
|
"""
|
||||||
|
StatusViewGuard.ROUTES = routes = ["route1", "route2"]
|
||||||
|
assert StatusViewGuard.routes(configuration) == routes
|
||||||
|
|
||||||
|
|
||||||
|
def test_routes_empty(configuration: Configuration) -> None:
|
||||||
|
"""
|
||||||
|
must return empty routes list if option is set
|
||||||
|
"""
|
||||||
|
StatusViewGuard.ROUTES = ["route1", "route2"]
|
||||||
|
configuration.set_option("web", "service_only", "yes")
|
||||||
|
assert StatusViewGuard.routes(configuration) == []
|
@ -6,6 +6,7 @@ from aiohttp.web import HTTPBadRequest, HTTPNotFound
|
|||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.models.repository_id import RepositoryId
|
from ahriman.models.repository_id import RepositoryId
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
@ -69,6 +70,15 @@ async def test_get_permission(base: BaseView) -> None:
|
|||||||
assert await base.get_permission(request) == UserAccess.Unauthorized
|
assert await base.get_permission(request) == UserAccess.Unauthorized
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_routes(configuration: Configuration, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must return list of available routes
|
||||||
|
"""
|
||||||
|
routes = ["route1", "route2"]
|
||||||
|
mocker.patch.object(BaseView, "ROUTES", routes)
|
||||||
|
assert BaseView.routes(configuration) == routes
|
||||||
|
|
||||||
|
|
||||||
def test_get_non_empty() -> None:
|
def test_get_non_empty() -> None:
|
||||||
"""
|
"""
|
||||||
must correctly extract non-empty values
|
must correctly extract non-empty values
|
||||||
|
Loading…
Reference in New Issue
Block a user