From 875bfc0823f8b2f06796380fa4e41742415ef847 Mon Sep 17 00:00:00 2001 From: Evgeniy Alekseev Date: Sat, 11 Sep 2021 16:34:43 +0300 Subject: [PATCH] add static files support and cookie expiration settings --- Makefile | 2 +- docs/configuration.md | 2 ++ package/etc/ahriman.ini | 2 ++ package/lib/systemd/system/ahriman-web@.service | 3 --- package/share/ahriman/build-status.jinja2 | 2 ++ package/share/ahriman/static/favicon.ico | Bin 0 -> 5832 bytes setup.py | 3 +++ src/ahriman/core/auth/auth.py | 5 +++-- src/ahriman/web/middlewares/auth_handler.py | 2 +- src/ahriman/web/routes.py | 6 +++++- src/ahriman/web/web.py | 2 +- tests/ahriman/core/upload/test_s3.py | 1 + .../ahriman/web/middlewares/test_auth_handler.py | 7 ++----- tests/ahriman/web/test_routes.py | 5 +++-- tests/ahriman/web/views/test_views_index.py | 8 ++++++++ tests/testresources/core/ahriman.ini | 1 + 16 files changed, 35 insertions(+), 16 deletions(-) create mode 100644 package/share/ahriman/static/favicon.ico diff --git a/Makefile b/Makefile index 7b15133c..e81dbd0d 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ PROJECT := ahriman -FILES := AUTHORS COPYING CONFIGURING.md README.md docs package src setup.cfg setup.py +FILES := AUTHORS COPYING README.md docs package src setup.cfg setup.py TARGET_FILES := $(addprefix $(PROJECT)/, $(FILES)) IGNORE_FILES := package/archlinux src/.mypy_cache diff --git a/docs/configuration.md b/docs/configuration.md index 4d113643..7f3f2827 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -26,6 +26,7 @@ Base authorization settings. * `allow_read_only` - allow requesting read only pages without authorization, boolean, required. * `allowed_paths` - URI paths (exact match) which can be accessed without authorization, space separated list of strings, optional. * `allowed_paths_groups` - URI paths prefixes which can be accessed without authorization, space separated list of strings, optional. +* `max_age` - parameter which controls both cookie expiration and token expiration inside the service, integer, optional, default is 7 days. * `salt` - password hash salt, string, required in case if authorization enabled (automatically generated by `create-user` subcommand). ## `auth:*` groups @@ -124,5 +125,6 @@ Web server settings. If any of `host`/`port` is not set, web integration will be * `host` - host to bind, string, optional. * `password` - password to authorize in web service in order to update service status, string, required in case if authorization enabled. * `port` - port to bind, int, optional. +* `static_path` - path to directory with static files, string, required. * `templates` - path to templates directory, string, required. * `username` - username to authorize in web service in order to update service status, string, required in case if authorization enabled. diff --git a/package/etc/ahriman.ini b/package/etc/ahriman.ini index 70adc524..f4c22a7a 100644 --- a/package/etc/ahriman.ini +++ b/package/etc/ahriman.ini @@ -11,6 +11,7 @@ root = / [auth] target = disabled allow_read_only = yes +max_age = 604800 [build] archbuild_flags = @@ -49,4 +50,5 @@ chunk_size = 8388608 [web] host = 127.0.0.1 +static_path = /usr/share/ahriman/static templates = /usr/share/ahriman \ No newline at end of file diff --git a/package/lib/systemd/system/ahriman-web@.service b/package/lib/systemd/system/ahriman-web@.service index b399b162..77273c75 100644 --- a/package/lib/systemd/system/ahriman-web@.service +++ b/package/lib/systemd/system/ahriman-web@.service @@ -8,8 +8,5 @@ ExecStart=/usr/bin/ahriman --architecture %i web User=ahriman Group=ahriman -KillSignal=SIGQUIT -SuccessExitStatus=SIGQUIT - [Install] WantedBy=multi-user.target \ No newline at end of file diff --git a/package/share/ahriman/build-status.jinja2 b/package/share/ahriman/build-status.jinja2 index 35520e01..d1cc2a7a 100644 --- a/package/share/ahriman/build-status.jinja2 +++ b/package/share/ahriman/build-status.jinja2 @@ -5,6 +5,8 @@ + + {% include "utils/style.jinja2" %} diff --git a/package/share/ahriman/static/favicon.ico b/package/share/ahriman/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e0c066d49a0e3675495bcc9e68e1e411a0ad4c62 GIT binary patch literal 5832 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 Kb6Mw<&;$UJ{x(Md literal 0 HcmV?d00001 diff --git a/setup.py b/setup.py index 4104b16f..8e0eb5f1 100644 --- a/setup.py +++ b/setup.py @@ -74,6 +74,9 @@ setup( "package/share/ahriman/build-status/package-actions-modals.jinja2", "package/share/ahriman/build-status/package-actions-script.jinja2", ]), + ("share/ahriman/static", [ + "package/share/ahriman/static/favicon.ico", + ]), ("share/ahriman/utils", [ "package/share/ahriman/utils/bootstrap-scripts.jinja2", "package/share/ahriman/utils/style.jinja2", diff --git a/src/ahriman/core/auth/auth.py b/src/ahriman/core/auth/auth.py index 70de402e..c33982e9 100644 --- a/src/ahriman/core/auth/auth.py +++ b/src/ahriman/core/auth/auth.py @@ -36,8 +36,8 @@ class Auth: :cvar ALLOWED_PATHS_GROUPS: URI paths prefixes which can be accessed without authorization, predefined """ - ALLOWED_PATHS = {"/", "/favicon.ico", "/index.html"} - ALLOWED_PATHS_GROUPS = {"/user-api"} + ALLOWED_PATHS = {"/", "/index.html"} + ALLOWED_PATHS_GROUPS = {"/static", "/user-api"} def __init__(self, configuration: Configuration, provider: AuthSettings = AuthSettings.Disabled) -> None: """ @@ -51,6 +51,7 @@ class Auth: self.allowed_paths_groups = set(configuration.getlist("auth", "allowed_paths_groups")) self.allowed_paths_groups.update(self.ALLOWED_PATHS_GROUPS) self.enabled = provider.is_enabled + self.max_age = configuration.getint("auth", "max_age", fallback=7 * 24 * 3600) @classmethod def load(cls: Type[Auth], configuration: Configuration) -> Auth: diff --git a/src/ahriman/web/middlewares/auth_handler.py b/src/ahriman/web/middlewares/auth_handler.py index b8b51fd1..93a211b8 100644 --- a/src/ahriman/web/middlewares/auth_handler.py +++ b/src/ahriman/web/middlewares/auth_handler.py @@ -95,7 +95,7 @@ def setup_auth(application: web.Application, validator: Auth) -> web.Application """ fernet_key = fernet.Fernet.generate_key() secret_key = base64.urlsafe_b64decode(fernet_key) - storage = EncryptedCookieStorage(secret_key, cookie_name='API_SESSION') + storage = EncryptedCookieStorage(secret_key, cookie_name="API_SESSION", max_age=validator.max_age) setup_session(application, storage) authorization_policy = AuthorizationPolicy(validator) diff --git a/src/ahriman/web/routes.py b/src/ahriman/web/routes.py index 17c22d5c..980d2d1a 100644 --- a/src/ahriman/web/routes.py +++ b/src/ahriman/web/routes.py @@ -18,6 +18,7 @@ # along with this program. If not, see . # from aiohttp.web import Application +from pathlib import Path from ahriman.web.views.index import IndexView from ahriman.web.views.service.add import AddView @@ -31,7 +32,7 @@ from ahriman.web.views.user.login import LoginView from ahriman.web.views.user.logout import LogoutView -def setup_routes(application: Application) -> None: +def setup_routes(application: Application, static_path: Path) -> None: """ setup all defined routes @@ -64,10 +65,13 @@ def setup_routes(application: Application) -> None: POST /user-api/v1/logout logout from service :param application: web application instance + :param static_path: path to static files directory """ application.router.add_get("/", IndexView, allow_head=True) application.router.add_get("/index.html", IndexView, allow_head=True) + application.router.add_static("/static", static_path, follow_symlinks=True) + application.router.add_post("/service-api/v1/add", AddView) application.router.add_post("/service-api/v1/remove", RemoveView) diff --git a/src/ahriman/web/web.py b/src/ahriman/web/web.py index 36a2b989..94e56c5f 100644 --- a/src/ahriman/web/web.py +++ b/src/ahriman/web/web.py @@ -84,7 +84,7 @@ def setup_service(architecture: str, configuration: Configuration, spawner: Spaw application.middlewares.append(exception_handler(application.logger)) application.logger.info("setup routes") - setup_routes(application) + setup_routes(application, configuration.getpath("web", "static_path")) application.logger.info("setup templates") aiohttp_jinja2.setup(application, loader=jinja2.FileSystemLoader(configuration.getpath("web", "templates"))) diff --git a/tests/ahriman/core/upload/test_s3.py b/tests/ahriman/core/upload/test_s3.py index c8b3af81..110ed0e3 100644 --- a/tests/ahriman/core/upload/test_s3.py +++ b/tests/ahriman/core/upload/test_s3.py @@ -62,6 +62,7 @@ def test_get_local_files(s3: S3, resource_path_root: Path) -> None: Path("web/templates/build-status/login-modal.jinja2"), Path("web/templates/build-status/package-actions-modals.jinja2"), Path("web/templates/build-status/package-actions-script.jinja2"), + Path("web/templates/static/favicon.ico"), Path("web/templates/utils/bootstrap-scripts.jinja2"), Path("web/templates/utils/style.jinja2"), Path("web/templates/build-status.jinja2"), diff --git a/tests/ahriman/web/middlewares/test_auth_handler.py b/tests/ahriman/web/middlewares/test_auth_handler.py index 3640889a..d1789a42 100644 --- a/tests/ahriman/web/middlewares/test_auth_handler.py +++ b/tests/ahriman/web/middlewares/test_auth_handler.py @@ -88,14 +88,11 @@ async def test_auth_handler_write(auth: Auth, mocker: MockerFixture) -> None: check_permission_mock.assert_called_with(aiohttp_request, UserAccess.Write, aiohttp_request.path) -def test_setup_auth( - application_with_auth: web.Application, - configuration: Configuration, - mocker: MockerFixture) -> None: +def test_setup_auth(application_with_auth: web.Application, auth: Auth, mocker: MockerFixture) -> None: """ must setup authorization """ aiohttp_security_setup_mock = mocker.patch("aiohttp_security.setup") - application = setup_auth(application_with_auth, configuration) + application = setup_auth(application_with_auth, auth) assert application.get("validator") is not None aiohttp_security_setup_mock.assert_called_once() diff --git a/tests/ahriman/web/test_routes.py b/tests/ahriman/web/test_routes.py index 009344c4..b22d0bdd 100644 --- a/tests/ahriman/web/test_routes.py +++ b/tests/ahriman/web/test_routes.py @@ -1,11 +1,12 @@ from aiohttp import web +from ahriman.core.configuration import Configuration from ahriman.web.routes import setup_routes -def test_setup_routes(application: web.Application) -> None: +def test_setup_routes(application: web.Application, configuration: Configuration) -> None: """ must generate non empty list of routes """ - setup_routes(application) + setup_routes(application, configuration.getpath("web", "static_path")) assert application.router.routes() diff --git a/tests/ahriman/web/views/test_views_index.py b/tests/ahriman/web/views/test_views_index.py index cda12b7b..61342667 100644 --- a/tests/ahriman/web/views/test_views_index.py +++ b/tests/ahriman/web/views/test_views_index.py @@ -26,3 +26,11 @@ async def test_get_without_auth(client: TestClient) -> None: response = await client.get("/") assert response.status == 200 assert await response.text() + + +async def test_get_static(client: TestClient) -> None: + """ + must return static files + """ + response = await client.get("/static/favicon.ico") + assert response.status == 200 diff --git a/tests/testresources/core/ahriman.ini b/tests/testresources/core/ahriman.ini index 0c568715..95343437 100644 --- a/tests/testresources/core/ahriman.ini +++ b/tests/testresources/core/ahriman.ini @@ -59,4 +59,5 @@ secret_key = [web] host = 127.0.0.1 +static_path = ../web/templates/static templates = ../web/templates \ No newline at end of file