add ability to remove an user

also replace old user by new one before creation
This commit is contained in:
Evgenii Alekseev 2021-09-16 02:36:53 +03:00
parent e99c2b0c83
commit 41731ca359
6 changed files with 316 additions and 231 deletions

View File

@ -62,7 +62,6 @@ def _parser() -> argparse.ArgumentParser:
_set_check_parser(subparsers) _set_check_parser(subparsers)
_set_clean_parser(subparsers) _set_clean_parser(subparsers)
_set_config_parser(subparsers) _set_config_parser(subparsers)
_set_create_user_parser(subparsers)
_set_init_parser(subparsers) _set_init_parser(subparsers)
_set_key_import_parser(subparsers) _set_key_import_parser(subparsers)
_set_rebuild_parser(subparsers) _set_rebuild_parser(subparsers)
@ -76,6 +75,7 @@ def _parser() -> argparse.ArgumentParser:
_set_status_update_parser(subparsers) _set_status_update_parser(subparsers)
_set_sync_parser(subparsers) _set_sync_parser(subparsers)
_set_update_parser(subparsers) _set_update_parser(subparsers)
_set_user_parser(subparsers)
_set_web_parser(subparsers) _set_web_parser(subparsers)
return parser return parser
@ -141,31 +141,6 @@ def _set_config_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser return parser
def _set_create_user_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for create user subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser(
"create-user",
help="create user for web services",
description="create user for web services with password and role. In case if password was not entered it will be asked interactively",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("username", help="username for web service")
parser.add_argument("--as-service", help="add user as service user", action="store_true")
parser.add_argument("-r", "--role", help="user role", type=UserAccess, choices=UserAccess, default=UserAccess.Read)
parser.add_argument("-p", "--password", help="user password")
parser.set_defaults(
handler=handlers.CreateUser,
architecture=[""],
lock=None,
no_log=True,
no_report=True,
unsafe=True)
return parser
def _set_init_parser(root: SubParserAction) -> argparse.ArgumentParser: def _set_init_parser(root: SubParserAction) -> argparse.ArgumentParser:
""" """
add parser for init subcommand add parser for init subcommand
@ -359,6 +334,32 @@ def _set_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser return parser
def _set_user_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for create user subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser(
"user",
help="manage users for web services",
description="manage users for web services with password and role. In case if password was not entered it will be asked interactively",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("username", help="username for web service")
parser.add_argument("--as-service", help="add user as service user", action="store_true")
parser.add_argument(
"-a",
"--access",
help="user access level",
type=UserAccess,
choices=UserAccess,
default=UserAccess.Read)
parser.add_argument("-p", "--password", help="user password")
parser.add_argument("-r", "--remove", help="remove user from configuration", action="store_true")
parser.set_defaults(handler=handlers.User, architecture=[""], lock=None, no_log=True, no_report=True, unsafe=True)
return parser
def _set_web_parser(root: SubParserAction) -> argparse.ArgumentParser: def _set_web_parser(root: SubParserAction) -> argparse.ArgumentParser:
""" """
add parser for web subcommand add parser for web subcommand

View File

@ -21,7 +21,6 @@ from ahriman.application.handlers.handler import Handler
from ahriman.application.handlers.add import Add from ahriman.application.handlers.add import Add
from ahriman.application.handlers.clean import Clean from ahriman.application.handlers.clean import Clean
from ahriman.application.handlers.create_user import CreateUser
from ahriman.application.handlers.dump import Dump from ahriman.application.handlers.dump import Dump
from ahriman.application.handlers.init import Init from ahriman.application.handlers.init import Init
from ahriman.application.handlers.key_import import KeyImport from ahriman.application.handlers.key_import import KeyImport
@ -36,4 +35,5 @@ from ahriman.application.handlers.status import Status
from ahriman.application.handlers.status_update import StatusUpdate from ahriman.application.handlers.status_update import StatusUpdate
from ahriman.application.handlers.sync import Sync from ahriman.application.handlers.sync import Sync
from ahriman.application.handlers.update import Update from ahriman.application.handlers.update import Update
from ahriman.application.handlers.user import User
from ahriman.application.handlers.web import Web from ahriman.application.handlers.web import Web

View File

@ -25,12 +25,13 @@ from typing import Type
from ahriman.application.handlers.handler import Handler from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.models.user import User from ahriman.models.user import User as MUser
from ahriman.models.user_access import UserAccess
class CreateUser(Handler): class User(Handler):
""" """
create user handler user management handler
""" """
@classmethod @classmethod
@ -43,13 +44,30 @@ class CreateUser(Handler):
:param configuration: configuration instance :param configuration: configuration instance
:param no_report: force disable reporting :param no_report: force disable reporting
""" """
salt = CreateUser.get_salt(configuration) salt = User.get_salt(configuration)
user = CreateUser.create_user(args) user = User.create_user(args)
auth_configuration = CreateUser.get_auth_configuration(configuration.include) auth_configuration = User.get_auth_configuration(configuration.include)
CreateUser.create_configuration(auth_configuration, user, salt, args.as_service)
User.clear_user(auth_configuration, user)
if not args.remove:
User.create_configuration(auth_configuration, user, salt, args.as_service)
User.write_configuration(configuration)
@staticmethod @staticmethod
def create_configuration(configuration: Configuration, user: User, salt: str, as_service_user: bool) -> None: def clear_user(configuration: Configuration, user: MUser) -> None:
"""
remove user user from configuration file in case if it exists
:param configuration: configuration instance
:param user: user descriptor
"""
for role in UserAccess:
section = Configuration.section_name("auth", role.value)
if not configuration.has_option(section, user.username):
continue
configuration.remove_option(section, user.username)
@staticmethod
def create_configuration(configuration: Configuration, user: MUser, salt: str, as_service_user: bool) -> None:
""" """
put new user to configuration put new user to configuration
:param configuration: configuration instance :param configuration: configuration instance
@ -65,19 +83,14 @@ class CreateUser(Handler):
configuration.set_option("web", "username", user.username) configuration.set_option("web", "username", user.username)
configuration.set_option("web", "password", user.password) configuration.set_option("web", "password", user.password)
if configuration.path is None:
return
with configuration.path.open("w") as ahriman_configuration:
configuration.write(ahriman_configuration)
@staticmethod @staticmethod
def create_user(args: argparse.Namespace) -> User: def create_user(args: argparse.Namespace) -> MUser:
""" """
create user descriptor from arguments create user descriptor from arguments
:param args: command line args :param args: command line args
:return: built user descriptor :return: built user descriptor
""" """
user = User(args.username, args.password, args.role) user = MUser(args.username, args.password, args.access)
if user.password is None: if user.password is None:
user.password = getpass.getpass() user.password = getpass.getpass()
return user return user
@ -91,7 +104,6 @@ class CreateUser(Handler):
""" """
target = include_path / "auth.ini" target = include_path / "auth.ini"
configuration = Configuration() configuration = Configuration()
if target.is_file(): # load current configuration in case if it exists
configuration.load(target) configuration.load(target)
return configuration return configuration
@ -107,4 +119,16 @@ class CreateUser(Handler):
salt = configuration.get("auth", "salt", fallback=None) salt = configuration.get("auth", "salt", fallback=None)
if salt: if salt:
return salt return salt
return User.generate_password(salt_length) return MUser.generate_password(salt_length)
@staticmethod
def write_configuration(configuration: Configuration) -> None:
"""
write configuration file
:param configuration: configuration instance
"""
if configuration.path is None:
return # should never happen actually
with configuration.path.open("w") as ahriman_configuration:
configuration.write(ahriman_configuration)
configuration.path.chmod(0o600)

View File

@ -1,164 +0,0 @@
import argparse
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from unittest import mock
from ahriman.application.handlers import CreateUser
from ahriman.core.configuration import Configuration
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
"""
default arguments for these test cases
:param args: command line arguments fixture
:return: generated arguments for these test cases
"""
args.username = "user"
args.password = "pa55w0rd"
args.role = UserAccess.Read
args.as_service = False
return args
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
get_auth_configuration_mock = mocker.patch("ahriman.application.handlers.CreateUser.get_auth_configuration")
create_configuration_mock = mocker.patch("ahriman.application.handlers.CreateUser.create_configuration")
create_user = mocker.patch("ahriman.application.handlers.CreateUser.create_user")
get_salt_mock = mocker.patch("ahriman.application.handlers.CreateUser.get_salt")
CreateUser.run(args, "x86_64", configuration, True)
get_auth_configuration_mock.assert_called_once()
create_configuration_mock.assert_called_once()
create_user.assert_called_once()
get_salt_mock.assert_called_once()
def test_create_configuration(configuration: Configuration, user: User, mocker: MockerFixture) -> None:
"""
must correctly create configuration file
"""
section = Configuration.section_name("auth", user.access.value)
mocker.patch("pathlib.Path.open")
set_mock = mocker.patch("ahriman.core.configuration.Configuration.set_option")
write_mock = mocker.patch("ahriman.core.configuration.Configuration.write")
CreateUser.create_configuration(configuration, user, "salt", False)
write_mock.assert_called_once()
set_mock.assert_has_calls([
mock.call("auth", "salt", pytest.helpers.anyvar(str)),
mock.call(section, user.username, pytest.helpers.anyvar(str))
])
def test_create_configuration_not_loaded(configuration: Configuration, user: User, mocker: MockerFixture) -> None:
"""
must do nothing in case if configuration is not loaded
"""
configuration.path = None
mocker.patch("pathlib.Path.open")
write_mock = mocker.patch("ahriman.core.configuration.Configuration.write")
CreateUser.create_configuration(configuration, user, "salt", False)
write_mock.assert_not_called()
def test_create_configuration_user_exists(configuration: Configuration, user: User, mocker: MockerFixture) -> None:
"""
must correctly update configuration file if user already exists
"""
section = Configuration.section_name("auth", user.access.value)
configuration.set_option(section, user.username, "")
mocker.patch("pathlib.Path.open")
mocker.patch("ahriman.core.configuration.Configuration.write")
CreateUser.create_configuration(configuration, user, "salt", False)
generated = User.from_option(user.username, configuration.get(section, user.username))
assert generated.check_credentials(user.password, configuration.get("auth", "salt"))
def test_create_configuration_with_plain_password(
configuration: Configuration,
user: User,
mocker: MockerFixture) -> None:
"""
must set plain text password and user for the service
"""
section = Configuration.section_name("auth", user.access.value)
mocker.patch("pathlib.Path.open")
mocker.patch("ahriman.core.configuration.Configuration.write")
CreateUser.create_configuration(configuration, user, "salt", True)
generated = User.from_option(user.username, configuration.get(section, user.username))
service = User.from_option(configuration.get("web", "username"), configuration.get("web", "password"))
assert generated.username == service.username
assert generated.check_credentials(service.password, configuration.get("auth", "salt"))
def test_create_user(args: argparse.Namespace, user: User) -> None:
"""
must create user
"""
args = _default_args(args)
generated = CreateUser.create_user(args)
assert generated.username == user.username
assert generated.access == user.access
def test_create_user_getpass(args: argparse.Namespace, mocker: MockerFixture) -> None:
"""
must create user and get password from command line
"""
args = _default_args(args)
args.password = None
getpass_mock = mocker.patch("getpass.getpass", return_value="password")
generated = CreateUser.create_user(args)
getpass_mock.assert_called_once()
assert generated.password == "password"
def test_get_salt_read(configuration: Configuration) -> None:
"""
must read salt from configuration
"""
assert CreateUser.get_salt(configuration) == "salt"
def test_get_salt_generate(configuration: Configuration) -> None:
"""
must generate salt if it does not exist
"""
configuration.remove_option("auth", "salt")
salt = CreateUser.get_salt(configuration, 16)
assert salt
assert len(salt) == 16
def test_get_auth_configuration() -> None:
"""
must load empty configuration
"""
assert CreateUser.get_auth_configuration(Path("path"))
def test_get_auth_configuration_exists(mocker: MockerFixture) -> None:
"""
must load configuration from filesystem
"""
mocker.patch("pathlib.Path.open")
mocker.patch("pathlib.Path.is_file", return_value=True)
read_mock = mocker.patch("ahriman.core.configuration.Configuration.read")
CreateUser.get_auth_configuration(Path("path"))
read_mock.assert_called_once()

View File

@ -0,0 +1,224 @@
import argparse
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from unittest import mock
from ahriman.application.handlers import User
from ahriman.core.configuration import Configuration
from ahriman.models.user import User as MUser
from ahriman.models.user_access import UserAccess
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
"""
default arguments for these test cases
:param args: command line arguments fixture
:return: generated arguments for these test cases
"""
args.username = "user"
args.password = "pa55w0rd"
args.access = UserAccess.Read
args.as_service = False
args.remove = False
return args
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
get_auth_configuration_mock = mocker.patch("ahriman.application.handlers.User.get_auth_configuration")
create_configuration_mock = mocker.patch("ahriman.application.handlers.User.create_configuration")
write_configuration_mock = mocker.patch("ahriman.application.handlers.User.write_configuration")
create_user = mocker.patch("ahriman.application.handlers.User.create_user")
get_salt_mock = mocker.patch("ahriman.application.handlers.User.get_salt")
User.run(args, "x86_64", configuration, True)
get_auth_configuration_mock.assert_called_once()
create_configuration_mock.assert_called_once()
create_user.assert_called_once()
get_salt_mock.assert_called_once()
write_configuration_mock.assert_called_once()
def test_run_remove(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must remove user if remove flag supplied
"""
args = _default_args(args)
args.remove = True
get_auth_configuration_mock = mocker.patch("ahriman.application.handlers.User.get_auth_configuration")
create_configuration_mock = mocker.patch("ahriman.application.handlers.User.create_configuration")
write_configuration_mock = mocker.patch("ahriman.application.handlers.User.write_configuration")
User.run(args, "x86_64", configuration, True)
get_auth_configuration_mock.assert_called_once()
create_configuration_mock.assert_not_called()
write_configuration_mock.assert_called_once()
def test_clear_user(configuration: Configuration, user: MUser) -> None:
"""
must clear user from configuration
"""
section = Configuration.section_name("auth", user.access.value)
configuration.set_option(section, user.username, user.password)
User.clear_user(configuration, user)
assert configuration.get(section, user.username, fallback=None) is None
def test_clear_user_multiple_sections(configuration: Configuration, user: MUser) -> None:
"""
must clear user from configuration from all sections
"""
for role in UserAccess:
section = Configuration.section_name("auth", role.value)
configuration.set_option(section, user.username, user.password)
User.clear_user(configuration, user)
for role in UserAccess:
section = Configuration.section_name("auth", role.value)
assert configuration.get(section, user.username, fallback=None) is None
def test_create_configuration(configuration: Configuration, user: MUser, mocker: MockerFixture) -> None:
"""
must correctly create configuration file
"""
section = Configuration.section_name("auth", user.access.value)
mocker.patch("pathlib.Path.open")
set_mock = mocker.patch("ahriman.core.configuration.Configuration.set_option")
User.create_configuration(configuration, user, "salt", False)
set_mock.assert_has_calls([
mock.call("auth", "salt", pytest.helpers.anyvar(str)),
mock.call(section, user.username, pytest.helpers.anyvar(str))
])
def test_create_configuration_user_exists(configuration: Configuration, user: MUser, mocker: MockerFixture) -> None:
"""
must correctly update configuration file if user already exists
"""
section = Configuration.section_name("auth", user.access.value)
configuration.set_option(section, user.username, "")
mocker.patch("pathlib.Path.open")
User.create_configuration(configuration, user, "salt", False)
generated = MUser.from_option(user.username, configuration.get(section, user.username))
assert generated.check_credentials(user.password, configuration.get("auth", "salt"))
def test_create_configuration_with_plain_password(
configuration: Configuration,
user: MUser,
mocker: MockerFixture) -> None:
"""
must set plain text password and user for the service
"""
section = Configuration.section_name("auth", user.access.value)
mocker.patch("pathlib.Path.open")
User.create_configuration(configuration, user, "salt", True)
generated = MUser.from_option(user.username, configuration.get(section, user.username))
service = MUser.from_option(configuration.get("web", "username"), configuration.get("web", "password"))
assert generated.username == service.username
assert generated.check_credentials(service.password, configuration.get("auth", "salt"))
def test_create_user(args: argparse.Namespace, user: MUser) -> None:
"""
must create user
"""
args = _default_args(args)
generated = User.create_user(args)
assert generated.username == user.username
assert generated.access == user.access
def test_create_user_getpass(args: argparse.Namespace, mocker: MockerFixture) -> None:
"""
must create user and get password from command line
"""
args = _default_args(args)
args.password = None
getpass_mock = mocker.patch("getpass.getpass", return_value="password")
generated = User.create_user(args)
getpass_mock.assert_called_once()
assert generated.password == "password"
def test_get_salt_read(configuration: Configuration) -> None:
"""
must read salt from configuration
"""
assert User.get_salt(configuration) == "salt"
def test_get_salt_generate(configuration: Configuration) -> None:
"""
must generate salt if it does not exist
"""
configuration.remove_option("auth", "salt")
salt = User.get_salt(configuration, 16)
assert salt
assert len(salt) == 16
def test_get_auth_configuration_exists(mocker: MockerFixture) -> None:
"""
must load configuration from filesystem
"""
mocker.patch("pathlib.Path.open")
mocker.patch("pathlib.Path.is_file", return_value=True)
read_mock = mocker.patch("ahriman.core.configuration.Configuration.read")
assert User.get_auth_configuration(Path("path"))
read_mock.assert_called_once()
def test_get_auth_configuration_not_exists(mocker: MockerFixture) -> None:
"""
must try to load configuration from filesystem
"""
mocker.patch("pathlib.Path.open")
mocker.patch("pathlib.Path.is_file", return_value=False)
read_mock = mocker.patch("ahriman.core.configuration.Configuration.read")
assert User.get_auth_configuration(Path("path"))
read_mock.assert_called_once()
def test_write_configuration(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must write configuration
"""
mocker.patch("pathlib.Path.open")
write_mock = mocker.patch("ahriman.core.configuration.Configuration.write")
chmod_mock = mocker.patch("pathlib.Path.chmod")
User.write_configuration(configuration)
write_mock.assert_called_once()
chmod_mock.assert_called_once()
def test_write_configuration_not_loaded(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must do nothing in case if configuration is not loaded
"""
configuration.path = None
mocker.patch("pathlib.Path.open")
write_mock = mocker.patch("ahriman.core.configuration.Configuration.write")
chmod_mock = mocker.patch("pathlib.Path.chmod")
User.write_configuration(configuration)
write_mock.assert_not_called()
chmod_mock.assert_not_called()

View File

@ -84,28 +84,6 @@ def test_subparsers_config(parser: argparse.ArgumentParser) -> None:
assert args.unsafe assert args.unsafe
def test_subparsers_create_user(parser: argparse.ArgumentParser) -> None:
"""
create-user command must imply architecture, lock, no-log, no-report and unsafe
"""
args = parser.parse_args(["create-user", "username"])
assert args.architecture == [""]
assert args.lock is None
assert args.no_log
assert args.no_report
assert args.unsafe
def test_subparsers_create_user_option_role(parser: argparse.ArgumentParser) -> None:
"""
create-user command must convert role option to useraccess instance
"""
args = parser.parse_args(["create-user", "username"])
assert isinstance(args.role, UserAccess)
args = parser.parse_args(["create-user", "username", "--role", "write"])
assert isinstance(args.role, UserAccess)
def test_subparsers_init(parser: argparse.ArgumentParser) -> None: def test_subparsers_init(parser: argparse.ArgumentParser) -> None:
""" """
init command must imply no_report init command must imply no_report
@ -258,6 +236,28 @@ def test_subparsers_update(parser: argparse.ArgumentParser) -> None:
assert args.architecture == [] assert args.architecture == []
def test_subparsers_user(parser: argparse.ArgumentParser) -> None:
"""
user command must imply architecture, lock, no-log, no-report and unsafe
"""
args = parser.parse_args(["user", "username"])
assert args.architecture == [""]
assert args.lock is None
assert args.no_log
assert args.no_report
assert args.unsafe
def test_subparsers_user_option_role(parser: argparse.ArgumentParser) -> None:
"""
user command must convert role option to useraccess instance
"""
args = parser.parse_args(["user", "username"])
assert isinstance(args.access, UserAccess)
args = parser.parse_args(["user", "username", "--access", "write"])
assert isinstance(args.access, UserAccess)
def test_subparsers_web(parser: argparse.ArgumentParser) -> None: def test_subparsers_web(parser: argparse.ArgumentParser) -> None:
""" """
web command must imply lock, no_report and parser web command must imply lock, no_report and parser