diff --git a/CONFIGURING.md b/CONFIGURING.md index 1677f18a..bb32be76 100644 --- a/CONFIGURING.md +++ b/CONFIGURING.md @@ -22,9 +22,9 @@ libalpm and AUR related configuration. Base authorization settings. +* `target` - specifies authorization provider, string, optional, default `disabled`. Allowed values are `disabled`, `configuration`. * `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. -* `enabled` - enables web services authorization, boolean, optional, default `no`. * `salt` - password hash salt, string, required in case if authorization enabled (automatically generated by `create-user` subcommand). ## `auth:*` groups diff --git a/package/etc/ahriman.ini b/package/etc/ahriman.ini index 8f729305..a0872e17 100644 --- a/package/etc/ahriman.ini +++ b/package/etc/ahriman.ini @@ -9,7 +9,7 @@ repositories = core extra community multilib root = / [auth] -enabled = no +target = disabled [build] archbuild_flags = diff --git a/src/ahriman/core/auth/auth.py b/src/ahriman/core/auth/auth.py index ee81442a..9ec1a1d8 100644 --- a/src/ahriman/core/auth/auth.py +++ b/src/ahriman/core/auth/auth.py @@ -22,6 +22,7 @@ from __future__ import annotations from typing import Optional, Set, Type from ahriman.core.configuration import Configuration +from ahriman.models.auth_settings import AuthSettings from ahriman.models.user_access import UserAccess @@ -38,16 +39,17 @@ class Auth: ALLOWED_PATHS = {"/", "/favicon.ico", "/index.html", "/login", "/logout"} ALLOWED_PATHS_GROUPS: Set[str] = set() - def __init__(self, configuration: Configuration) -> None: + def __init__(self, configuration: Configuration, provider: AuthSettings = AuthSettings.Disabled) -> None: """ default constructor :param configuration: configuration instance + :param provider: authorization type definition """ self.allowed_paths = set(configuration.getlist("auth", "allowed_paths")) self.allowed_paths.update(self.ALLOWED_PATHS) self.allowed_paths_groups = set(configuration.getlist("auth", "allowed_paths_groups")) self.allowed_paths_groups.update(self.ALLOWED_PATHS_GROUPS) - self.enabled = configuration.getboolean("auth", "enabled", fallback=False) + self.enabled = provider.is_enabled @classmethod def load(cls: Type[Auth], configuration: Configuration) -> Auth: @@ -56,7 +58,8 @@ class Auth: :param configuration: configuration instance :return: authorization module according to current settings """ - if configuration.getboolean("auth", "enabled", fallback=False): + provider = AuthSettings.from_option(configuration.get("auth", "target", fallback="disabled")) + if provider == AuthSettings.Configuration: from ahriman.core.auth.mapping_auth import MappingAuth return MappingAuth(configuration) return cls(configuration) diff --git a/src/ahriman/core/auth/mapping_auth.py b/src/ahriman/core/auth/mapping_auth.py index 873e591a..3a25b9d3 100644 --- a/src/ahriman/core/auth/mapping_auth.py +++ b/src/ahriman/core/auth/mapping_auth.py @@ -22,6 +22,7 @@ from typing import Dict, Optional from ahriman.core.auth.auth import Auth from ahriman.core.configuration import Configuration from ahriman.core.exceptions import DuplicateUser +from ahriman.models.auth_settings import AuthSettings from ahriman.models.user import User from ahriman.models.user_access import UserAccess @@ -33,12 +34,13 @@ class MappingAuth(Auth): :ivar _users: map of username to its descriptor """ - def __init__(self, configuration: Configuration) -> None: + def __init__(self, configuration: Configuration, provider: AuthSettings = AuthSettings.Configuration) -> None: """ default constructor :param configuration: configuration instance + :param provider: authorization type definition """ - Auth.__init__(self, configuration) + Auth.__init__(self, configuration, provider) self.salt = configuration.get("auth", "salt") self._users = self.get_users(configuration) diff --git a/src/ahriman/models/auth_settings.py b/src/ahriman/models/auth_settings.py new file mode 100644 index 00000000..46294e30 --- /dev/null +++ b/src/ahriman/models/auth_settings.py @@ -0,0 +1,58 @@ +# +# Copyright (c) 2021 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 . +# +from __future__ import annotations + +from enum import Enum, auto +from typing import Type + +from ahriman.core.exceptions import InvalidOption + + +class AuthSettings(Enum): + """ + web authorization type + :cvar Disabled: authorization is disabled + :cvar Configuration: configuration based authorization + """ + + Disabled = auto() + Configuration = auto() + + @classmethod + def from_option(cls: Type[AuthSettings], value: str) -> AuthSettings: + """ + construct value from configuration + :param value: configuration value + :return: parsed value + """ + if value.lower() in ("disabled", "no"): + return cls.Disabled + if value.lower() in ("configuration", "mapping"): + return cls.Configuration + raise InvalidOption(value) + + @property + def is_enabled(self) -> bool: + """ + :return: False in case if authorization is disabled and True otherwise + """ + if self == AuthSettings.Disabled: + return False + return True diff --git a/tests/ahriman/core/auth/conftest.py b/tests/ahriman/core/auth/conftest.py index 71b2a49d..4430b262 100644 --- a/tests/ahriman/core/auth/conftest.py +++ b/tests/ahriman/core/auth/conftest.py @@ -10,5 +10,4 @@ def mapping_auth(configuration: Configuration) -> MappingAuth: auth provider fixture :return: auth service instance """ - configuration.set_option("auth", "enabled", "yes") return MappingAuth(configuration) diff --git a/tests/ahriman/core/auth/test_auth.py b/tests/ahriman/core/auth/test_auth.py index 6e8a2051..fe42b1d4 100644 --- a/tests/ahriman/core/auth/test_auth.py +++ b/tests/ahriman/core/auth/test_auth.py @@ -9,7 +9,7 @@ def test_load_dummy(configuration: Configuration) -> None: """ must load dummy validator if authorization is not enabled """ - configuration.set_option("auth", "enabled", "no") + configuration.set_option("auth", "target", "disabled") auth = Auth.load(configuration) assert isinstance(auth, Auth) @@ -26,7 +26,7 @@ def test_load_mapping(configuration: Configuration) -> None: """ must load mapping validator if option set """ - configuration.set_option("auth", "enabled", "yes") + configuration.set_option("auth", "target", "configuration") auth = Auth.load(configuration) assert isinstance(auth, MappingAuth) diff --git a/tests/ahriman/models/test_auth_settings.py b/tests/ahriman/models/test_auth_settings.py new file mode 100644 index 00000000..c5d6ed9b --- /dev/null +++ b/tests/ahriman/models/test_auth_settings.py @@ -0,0 +1,36 @@ +import pytest + +from ahriman.core.exceptions import InvalidOption +from ahriman.models.auth_settings import AuthSettings + + +def test_from_option_invalid() -> None: + """ + must raise exception on invalid option + """ + with pytest.raises(InvalidOption, match=".* `invalid`$"): + AuthSettings.from_option("invalid") + + +def test_from_option_valid() -> None: + """ + must return value from valid options + """ + assert AuthSettings.from_option("disabled") == AuthSettings.Disabled + assert AuthSettings.from_option("DISABLED") == AuthSettings.Disabled + assert AuthSettings.from_option("no") == AuthSettings.Disabled + assert AuthSettings.from_option("NO") == AuthSettings.Disabled + + assert AuthSettings.from_option("configuration") == AuthSettings.Configuration + assert AuthSettings.from_option("ConFigUration") == AuthSettings.Configuration + assert AuthSettings.from_option("mapping") == AuthSettings.Configuration + assert AuthSettings.from_option("MAPPing") == AuthSettings.Configuration + + +def test_is_enabled() -> None: + """ + must mark as disabled authorization for disabled and enabled otherwise + """ + assert not AuthSettings.Disabled.is_enabled + for option in filter(lambda o: o != AuthSettings.Disabled, AuthSettings): + assert option.is_enabled diff --git a/tests/ahriman/web/conftest.py b/tests/ahriman/web/conftest.py index 44cf4e9e..57d26139 100644 --- a/tests/ahriman/web/conftest.py +++ b/tests/ahriman/web/conftest.py @@ -32,7 +32,7 @@ def application_with_auth(configuration: Configuration, user: User, mocker: Mock :param mocker: mocker object :return: application test instance """ - configuration.set_option("auth", "enabled", "yes") + configuration.set_option("auth", "target", "configuration") mocker.patch.object(ahriman.core.auth.helpers, "_has_aiohttp_security", True) mocker.patch("pathlib.Path.mkdir") application = setup_service("x86_64", configuration) diff --git a/tests/ahriman/web/middlewares/conftest.py b/tests/ahriman/web/middlewares/conftest.py index 0f2e7e8a..716c29cd 100644 --- a/tests/ahriman/web/middlewares/conftest.py +++ b/tests/ahriman/web/middlewares/conftest.py @@ -25,7 +25,7 @@ def authorization_policy(configuration: Configuration, user: User) -> Authorizat fixture for authorization policy :return: authorization policy fixture """ - configuration.set_option("auth", "enabled", "yes") + configuration.set_option("auth", "target", "configuration") validator = Auth.load(configuration) policy = AuthorizationPolicy(validator) policy.validator._users = {user.username: user}