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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 Sez=GL0000 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