From 05aa54ac0f2f055aa5401a0d11d8069ce960a894 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 26 May 2020 06:03:49 +0200 Subject: [PATCH 01/10] [mod] refactor the whole argument parsing flow into a component oriented way --- src/yunohost/app.py | 293 +++++++++++------- .../tests/test_apps_arguments_parsing.py | 7 +- 2 files changed, 186 insertions(+), 114 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 0b16123cb..027a3a558 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2391,6 +2391,184 @@ def _parse_args_for_action(action, args={}): return _parse_args_in_yunohost_format(args, action_args) +class Question: + "empty class to store questions information" + + +class YunoHostArgumentFormatParser(object): + hide_user_input_in_prompt = False + + def parse_question(self, question, user_answers): + parsed_question = Question() + + parsed_question.name = question['name'] + parsed_question.default = question.get('default', None) + parsed_question.choices = question.get('choices', []) + parsed_question.optional = question.get('optional', False) + parsed_question.ask = question.get('ask') + parsed_question.value = user_answers.get(parsed_question.name) + + if parsed_question.ask is None: + parsed_question.ask = "Enter value for '%s':" % parsed_question.name + + return parsed_question + + def parse(self, question, user_answers): + question = self.parse_question(question, user_answers) + + if question.value is None: + text_for_user_input_in_cli = self._format_text_for_user_input_in_cli(question) + + try: + question.value = msignals.prompt(text_for_user_input_in_cli, self.hide_user_input_in_prompt) + except NotImplementedError: + question.value = None + + # we don't have an answer, check optional and default_value + if question.value is None: + if not question.optional and question.default is None: + raise YunohostError('app_argument_required', name=question.name) + else: + question.value = self.default_value if question.default is None else question.default + + # we have an answer, do some post checks + if question.value is not None: + if question.choices and question.value not in question.choices: + raise YunohostError('app_argument_choice_invalid', name=question.name, + choices=', '.join(question.choices)) + + # this is done to enforce a certain formating like for boolean + # by default it doesn't do anything + question.value = self._post_parse_value(question) + + return (question.value, self.argument_type) + + def _format_text_for_user_input_in_cli(self, question): + text_for_user_input_in_cli = _value_for_locale(question.ask) + + if question.default is not None: + text_for_user_input_in_cli += ' (default: {0})'.format(question.default) + + if question.choices: + text_for_user_input_in_cli += ' [{0}]'.format(' | '.join(question.choices)) + + return text_for_user_input_in_cli + + def _post_parse_value(self, question): + return question.value + + +class StringArgumentParser(YunoHostArgumentFormatParser): + argument_type = "string" + default_value = "" + + +class PasswordArgumentParser(YunoHostArgumentFormatParser): + hide_user_input_in_prompt = True + argument_type = "password" + default_value = "" + + +class PathArgumentParser(YunoHostArgumentFormatParser): + argument_type = "path" + default_value = "" + + +class BooleanArgumentParser(YunoHostArgumentFormatParser): + argument_type = "boolean" + default_value = False + + def parse_question(self, question, user_answers): + question = super(BooleanArgumentParser, self).parse_question(question, user_answers) + + if question.default is None: + question.default = False + + return question + + def _format_text_for_user_input_in_cli(self, question): + text_for_user_input_in_cli = _value_for_locale(question.ask) + + text_for_user_input_in_cli += " [yes | no]" + + if question.default is not None: + formatted_default = "yes" if question.default else "no" + text_for_user_input_in_cli += ' (default: {0})'.format(formatted_default) + + return text_for_user_input_in_cli + + def _post_parse_value(self, question): + if isinstance(question.value, bool): + return 1 if question.value else 0 + + if str(question.value).lower() in ["1", "yes", "y"]: + return 1 + + if str(question.value).lower() in ["0", "no", "n"]: + return 0 + + raise YunohostError('app_argument_choice_invalid', name=question.name, + choices='yes, no, y, n, 1, 0') + + +class DomainArgumentParser(YunoHostArgumentFormatParser): + argument_type = "domain" + + def parse_question(self, question, user_answers): + from yunohost.domain import domain_list, _get_maindomain + + question = super(DomainArgumentParser, self).parse_question(question, user_answers) + + if question.default is None: + question.default = _get_maindomain() + + question.choices = domain_list()["domains"] + + return question + + +class UserArgumentParser(YunoHostArgumentFormatParser): + argument_type = "user" + + def parse_question(self, question, user_answers): + from yunohost.user import user_list + + question = super(UserArgumentParser, self).parse_question(question, user_answers) + question.choices = user_list()["users"] + + return question + + +class AppArgumentParser(YunoHostArgumentFormatParser): + argument_type = "app" + + def parse_question(self, question, user_answers): + from yunohost.app import app_list + + question = super(AppArgumentParser, self).parse_question(question, user_answers) + question.choices = [x["id"] for x in app_list()["apps"]] + + return question + + +class DisplayTextArgumentParser(YunoHostArgumentFormatParser): + + def parse(self, question, user_answers): + print(question["ask"]) + + +ARGUMENTS_TYPE_PARSERS = { + "string": StringArgumentParser, + "password": PasswordArgumentParser, + "path": PathArgumentParser, + "boolean": BooleanArgumentParser, + "domain": DomainArgumentParser, + "user": UserArgumentParser, + "app": AppArgumentParser, + "display_text": DisplayTextArgumentParser, +} + + def _parse_args_in_yunohost_format(user_answers, argument_questions): """Parse arguments store in either manifest.json or actions.json or from a config panel against the user answers when they are present. @@ -2402,121 +2580,14 @@ def _parse_args_in_yunohost_format(user_answers, argument_questions): format from actions.json/toml, manifest.json/toml or config_panel.json/toml """ - from yunohost.domain import domain_list, _get_maindomain - from yunohost.user import user_list - parsed_answers_dict = OrderedDict() for question in argument_questions: - question_name = question['name'] - question_type = question.get('type', 'string') - question_default = question.get('default', None) - question_choices = question.get('choices', []) - question_value = None + parser = ARGUMENTS_TYPE_PARSERS[question.get("type", "string")]() - # Transpose default value for boolean type and set it to - # false if not defined. - if question_type == 'boolean': - question_default = 1 if question_default else 0 - - # do not print for webadmin - if question_type == 'display_text' and msettings.get('interface') != 'api': - print(_value_for_locale(question['ask'])) - continue - - # Attempt to retrieve argument value - if question_name in user_answers: - question_value = user_answers[question_name] - else: - if 'ask' in question: - # Retrieve proper ask string - text_for_user_input_in_cli = _value_for_locale(question['ask']) - - # Append extra strings - if question_type == 'boolean': - text_for_user_input_in_cli += ' [yes | no]' - elif question_choices: - text_for_user_input_in_cli += ' [{0}]'.format(' | '.join(question_choices)) - - if question_default is not None: - if question_type == 'boolean': - text_for_user_input_in_cli += ' (default: {0})'.format("yes" if question_default == 1 else "no") - else: - text_for_user_input_in_cli += ' (default: {0})'.format(question_default) - - # Check for a password argument - is_password = True if question_type == 'password' else False - - if question_type == 'domain': - question_default = _get_maindomain() - text_for_user_input_in_cli += ' (default: {0})'.format(question_default) - msignals.display(m18n.n('domains_available')) - for domain in domain_list()['domains']: - msignals.display("- {}".format(domain)) - - elif question_type == 'user': - msignals.display(m18n.n('users_available')) - for user in user_list()['users'].keys(): - msignals.display("- {}".format(user)) - - elif question_type == 'password': - msignals.display(m18n.n('good_practices_about_user_password')) - - try: - input_string = msignals.prompt(text_for_user_input_in_cli, is_password) - except NotImplementedError: - input_string = None - if (input_string == '' or input_string is None) \ - and question_default is not None: - question_value = question_default - else: - question_value = input_string - elif question_default is not None: - question_value = question_default - - # If the value is empty (none or '') - # then check if question is optional or not - if question_value is None or question_value == '': - if question.get("optional", False): - # Argument is optional, keep an empty value - # and that's all for this question! - parsed_answers_dict[question_name] = ('', question_type) - continue - else: - # The argument is required ! - raise YunohostError('app_argument_required', name=question_name) - - # Validate argument choice - if question_choices and question_value not in question_choices: - raise YunohostError('app_argument_choice_invalid', name=question_name, choices=', '.join(question_choices)) - - # Validate argument type - if question_type == 'domain': - if question_value not in domain_list()['domains']: - raise YunohostError('app_argument_invalid', name=question_name, error=m18n.n('domain_unknown')) - elif question_type == 'user': - if question_value not in user_list()["users"].keys(): - raise YunohostError('app_argument_invalid', name=question_name, error=m18n.n('user_unknown', user=question_value)) - elif question_type == 'app': - if not _is_installed(question_value): - raise YunohostError('app_argument_invalid', name=question_name, error=m18n.n('app_unknown')) - elif question_type == 'boolean': - if isinstance(question_value, bool): - question_value = 1 if question_value else 0 - else: - if str(question_value).lower() in ["1", "yes", "y"]: - question_value = 1 - elif str(question_value).lower() in ["0", "no", "n"]: - question_value = 0 - else: - raise YunohostError('app_argument_choice_invalid', name=question_name, choices='yes, no, y, n, 1, 0') - elif question_type == 'password': - forbidden_chars = "{}" - if any(char in question_value for char in forbidden_chars): - raise YunohostError('pattern_password_app', forbidden_chars=forbidden_chars) - from yunohost.utils.password import assert_password_is_strong_enough - assert_password_is_strong_enough('user', question_value) - parsed_answers_dict[question_name] = (question_value, question_type) + answer = parser.parse(question=question, user_answers=user_answers) + if answer is not None: + parsed_answers_dict[question["name"]] = answer return parsed_answers_dict diff --git a/src/yunohost/tests/test_apps_arguments_parsing.py b/src/yunohost/tests/test_apps_arguments_parsing.py index a3d5b7f09..c127cc414 100644 --- a/src/yunohost/tests/test_apps_arguments_parsing.py +++ b/src/yunohost/tests/test_apps_arguments_parsing.py @@ -705,15 +705,16 @@ def test_parse_args_in_yunohost_format_boolean_input_test_ask_with_default(): def test_parse_args_in_yunohost_format_domain_empty(): questions = [{"name": "some_domain", "type": "domain",}] + main_domain = "my_main_domain.com" + expected_result = OrderedDict({"some_domain": (main_domain, "domain")}) answers = {} with patch.object( domain, "_get_maindomain", return_value="my_main_domain.com" ), patch.object( - domain, "domain_list", return_value={"domains": ["my_main_domain.com"]} + domain, "domain_list", return_value={"domains": [main_domain]} ): - with pytest.raises(YunohostError): - _parse_args_in_yunohost_format(answers, questions) + assert _parse_args_in_yunohost_format(answers, questions) == expected_result def test_parse_args_in_yunohost_format_domain(): From 40dc8397c130581a73135f0c2fc6149fc30fa82a Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 1 Jun 2020 11:43:39 +0200 Subject: [PATCH 02/10] [mod] stop skipping tests that now works --- src/yunohost/tests/test_apps_arguments_parsing.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/yunohost/tests/test_apps_arguments_parsing.py b/src/yunohost/tests/test_apps_arguments_parsing.py index c127cc414..5eed5bdfe 100644 --- a/src/yunohost/tests/test_apps_arguments_parsing.py +++ b/src/yunohost/tests/test_apps_arguments_parsing.py @@ -70,7 +70,6 @@ def test_parse_args_in_yunohost_format_string_input(): assert _parse_args_in_yunohost_format(answers, questions) == expected_result -@pytest.mark.skip # that shit should work x( def test_parse_args_in_yunohost_format_string_input_no_ask(): questions = [{"name": "some_string",}] answers = {} @@ -96,7 +95,6 @@ def test_parse_args_in_yunohost_format_string_optional_with_input(): assert _parse_args_in_yunohost_format(answers, questions) == expected_result -@pytest.mark.skip # this should work without ask def test_parse_args_in_yunohost_format_string_optional_with_input_without_ask(): questions = [{"name": "some_string", "optional": True,}] answers = {} @@ -237,7 +235,6 @@ def test_parse_args_in_yunohost_format_password_input(): assert _parse_args_in_yunohost_format(answers, questions) == expected_result -@pytest.mark.skip # that shit should work x( def test_parse_args_in_yunohost_format_password_input_no_ask(): questions = [{"name": "some_password", "type": "password",}] answers = {} @@ -270,7 +267,6 @@ def test_parse_args_in_yunohost_format_password_optional_with_input(): assert _parse_args_in_yunohost_format(answers, questions) == expected_result -@pytest.mark.skip # this should work without ask def test_parse_args_in_yunohost_format_password_optional_with_input_without_ask(): questions = [{"name": "some_password", "type": "password", "optional": True,}] answers = {} @@ -388,7 +384,6 @@ def test_parse_args_in_yunohost_format_path_input(): assert _parse_args_in_yunohost_format(answers, questions) == expected_result -@pytest.mark.skip # that shit should work x( def test_parse_args_in_yunohost_format_path_input_no_ask(): questions = [{"name": "some_path", "type": "path",}] answers = {} @@ -416,7 +411,6 @@ def test_parse_args_in_yunohost_format_path_optional_with_input(): assert _parse_args_in_yunohost_format(answers, questions) == expected_result -@pytest.mark.skip # this should work without ask def test_parse_args_in_yunohost_format_path_optional_with_input_without_ask(): questions = [{"name": "some_path", "type": "path", "optional": True,}] answers = {} @@ -604,11 +598,10 @@ def test_parse_args_in_yunohost_format_boolean_input(): assert _parse_args_in_yunohost_format(answers, questions) == expected_result -@pytest.mark.skip # we should work def test_parse_args_in_yunohost_format_boolean_input_no_ask(): questions = [{"name": "some_boolean", "type": "boolean",}] answers = {} - expected_result = OrderedDict({"some_boolean": ("some_value", "boolean")}) + expected_result = OrderedDict({"some_boolean": (1, "boolean")}) with patch.object(msignals, "prompt", return_value="y"): assert _parse_args_in_yunohost_format(answers, questions) == expected_result @@ -660,7 +653,6 @@ def test_parse_args_in_yunohost_format_boolean_no_input_default(): assert _parse_args_in_yunohost_format(answers, questions) == expected_result -@pytest.mark.skip # we should raise def test_parse_args_in_yunohost_format_boolean_bad_default(): questions = [ { @@ -769,7 +761,6 @@ def test_parse_args_in_yunohost_format_domain_two_domains_wrong_answer(): _parse_args_in_yunohost_format(answers, questions) -@pytest.mark.skip # XXX should work def test_parse_args_in_yunohost_format_domain_two_domains_default_no_ask(): main_domain = "my_main_domain.com" other_domain = "some_other_domain.tld" From dc6a12f14b9ef3b1d27df97203cc925c73f5f79b Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 1 Jun 2020 12:02:04 +0200 Subject: [PATCH 03/10] [mod] password argument can't have a default value --- locales/en.json | 1 + src/yunohost/app.py | 8 ++++++++ src/yunohost/tests/test_apps_arguments_parsing.py | 1 - 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 851656532..5952e8b2a 100644 --- a/locales/en.json +++ b/locales/en.json @@ -13,6 +13,7 @@ "app_already_up_to_date": "{app:s} is already up-to-date", "app_argument_choice_invalid": "Use one of these choices '{choices:s}' for the argument '{name:s}'", "app_argument_invalid": "Pick a valid value for the argument '{name:s}': {error:s}", + "app_argument_password_no_default": "Error while parsing password argument '{name}': password argument can't have a default value for security reason", "app_argument_required": "Argument '{name:s}' is required", "app_change_url_failed_nginx_reload": "Could not reload NGINX. Here is the output of 'nginx -t':\n{nginx_errors:s}", "app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain:s}{path:s}'), nothing to do.", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 027a3a558..4ec043687 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2468,6 +2468,14 @@ class PasswordArgumentParser(YunoHostArgumentFormatParser): argument_type = "password" default_value = "" + def parse_question(self, question, user_answers): + question = super(PasswordArgumentParser, self).parse_question(question, user_answers) + + if question.default is not None: + raise YunohostError('app_argument_password_no_default', name=question.name) + + return question + class PathArgumentParser(YunoHostArgumentFormatParser): argument_type = "path" diff --git a/src/yunohost/tests/test_apps_arguments_parsing.py b/src/yunohost/tests/test_apps_arguments_parsing.py index 5eed5bdfe..7b835c410 100644 --- a/src/yunohost/tests/test_apps_arguments_parsing.py +++ b/src/yunohost/tests/test_apps_arguments_parsing.py @@ -276,7 +276,6 @@ def test_parse_args_in_yunohost_format_password_optional_with_input_without_ask( assert _parse_args_in_yunohost_format(answers, questions) == expected_result -@pytest.mark.skip # this should raises def test_parse_args_in_yunohost_format_password_no_input_default(): questions = [ { From ecb52b4f1187e6e977ede50f2748c1ed009d3586 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 1 Jun 2020 12:37:10 +0200 Subject: [PATCH 04/10] [fix] allow optional app argument --- src/yunohost/app.py | 2 +- src/yunohost/tests/test_apps_arguments_parsing.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 4ec043687..4abaaecca 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2429,7 +2429,7 @@ class YunoHostArgumentFormatParser(object): if not question.optional and question.default is None: raise YunohostError('app_argument_required', name=question.name) else: - question.value = self.default_value if question.default is None else question.default + question.value = getattr(self, "default_value", None) if question.default is None else question.default # we have an answer, do some post checks if question.value is not None: diff --git a/src/yunohost/tests/test_apps_arguments_parsing.py b/src/yunohost/tests/test_apps_arguments_parsing.py index 7b835c410..1ea73cc0a 100644 --- a/src/yunohost/tests/test_apps_arguments_parsing.py +++ b/src/yunohost/tests/test_apps_arguments_parsing.py @@ -1011,14 +1011,14 @@ def test_parse_args_in_yunohost_format_app_no_apps(): _parse_args_in_yunohost_format(answers, questions) -@pytest.mark.skip # XXX should work def test_parse_args_in_yunohost_format_app_no_apps_optional(): apps = [] questions = [{"name": "some_app", "type": "app", "optional": True}] answers = {} + expected_result = OrderedDict({"some_app": (None, "app")}) with patch.object(app, "app_list", return_value={"apps": apps}): - assert _parse_args_in_yunohost_format(answers, questions) == [] + assert _parse_args_in_yunohost_format(answers, questions) == expected_result def test_parse_args_in_yunohost_format_app(): From 49b91460654718c9637fdf6091220b433c46bbaf Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 3 Jul 2020 00:33:44 +0200 Subject: [PATCH 05/10] [fix] reintroduce custom exceptions for input fields type --- src/yunohost/app.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 4abaaecca..9c76e73eb 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2434,8 +2434,7 @@ class YunoHostArgumentFormatParser(object): # we have an answer, do some post checks if question.value is not None: if question.choices and question.value not in question.choices: - raise YunohostError('app_argument_choice_invalid', name=question.name, - choices=', '.join(question.choices)) + self._raise_invalide_answer(question) # this is done to enforce a certain formating like for boolean # by default it doesn't do anything @@ -2443,6 +2442,10 @@ class YunoHostArgumentFormatParser(object): return (question.value, self.argument_type) + def _raise_invalide_answer(self, question): + raise YunohostError('app_argument_choice_invalid', name=question.name, + choices=', '.join(question.choices)) + def _format_text_for_user_input_in_cli(self, question): text_for_user_input_in_cli = _value_for_locale(question.ask) @@ -2534,6 +2537,10 @@ class DomainArgumentParser(YunoHostArgumentFormatParser): return question + def _raise_invalide_answer(self, question): + raise YunohostError('app_argument_invalid', name=question.name, + error=m18n.n('domain_unknown')) + class UserArgumentParser(YunoHostArgumentFormatParser): argument_type = "user" @@ -2546,6 +2553,10 @@ class UserArgumentParser(YunoHostArgumentFormatParser): return question + def _raise_invalide_answer(self, question): + raise YunohostError('app_argument_invalid', name=question.name, + error=m18n.n('user_unknown', user=question.value)) + class AppArgumentParser(YunoHostArgumentFormatParser): argument_type = "app" @@ -2558,6 +2569,10 @@ class AppArgumentParser(YunoHostArgumentFormatParser): return question + def _raise_invalide_answer(self, question): + raise YunohostError('app_argument_invalid', name=question.name, + error=m18n.n('app_unknown')) + class DisplayTextArgumentParser(YunoHostArgumentFormatParser): From 2eb5f6b6da3d398f890c68240da2786852e01726 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 3 Jul 2020 00:49:14 +0200 Subject: [PATCH 06/10] [fix] re-put forbidding some chars in passwords --- src/yunohost/app.py | 7 +++++++ .../tests/test_apps_arguments_parsing.py | 17 ++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 9c76e73eb..a3244abe1 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2470,6 +2470,7 @@ class PasswordArgumentParser(YunoHostArgumentFormatParser): hide_user_input_in_prompt = True argument_type = "password" default_value = "" + forbidden_chars = "{}" def parse_question(self, question, user_answers): question = super(PasswordArgumentParser, self).parse_question(question, user_answers) @@ -2479,6 +2480,12 @@ class PasswordArgumentParser(YunoHostArgumentFormatParser): return question + def _post_parse_value(self, question): + if any(char in question.value for char in self.forbidden_chars): + raise YunohostError('pattern_password_app', forbidden_chars=self.forbidden_chars) + + return super(PasswordArgumentParser, self)._post_parse_value(question) + class PathArgumentParser(YunoHostArgumentFormatParser): argument_type = "path" diff --git a/src/yunohost/tests/test_apps_arguments_parsing.py b/src/yunohost/tests/test_apps_arguments_parsing.py index 1ea73cc0a..9576ce0bf 100644 --- a/src/yunohost/tests/test_apps_arguments_parsing.py +++ b/src/yunohost/tests/test_apps_arguments_parsing.py @@ -8,7 +8,7 @@ from collections import OrderedDict from moulinette import msignals from yunohost import domain, user, app -from yunohost.app import _parse_args_in_yunohost_format +from yunohost.app import _parse_args_in_yunohost_format, PasswordArgumentParser from yunohost.utils.error import YunohostError @@ -359,6 +359,21 @@ def test_parse_args_in_yunohost_format_password_input_test_ask_with_help(): assert help_text in prompt.call_args[0][0] +def test_parse_args_in_yunohost_format_password_bad_chars(): + questions = [ + { + "name": "some_password", + "type": "password", + "ask": "some question", + "example": "some_value", + } + ] + + for i in PasswordArgumentParser.forbidden_chars: + with pytest.raises(YunohostError): + _parse_args_in_yunohost_format({"some_password": i * 8}, questions) + + def test_parse_args_in_yunohost_format_path(): questions = [{"name": "some_path", "type": "path",}] answers = {"some_path": "some_value"} From bcd2364d74378753b7d16bc5db9f7b2113706196 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 3 Jul 2020 00:57:09 +0200 Subject: [PATCH 07/10] [fix] re-put assert password is strong enough --- src/yunohost/app.py | 3 +++ .../tests/test_apps_arguments_parsing.py | 23 +++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index a3244abe1..1aba439e8 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2484,6 +2484,9 @@ class PasswordArgumentParser(YunoHostArgumentFormatParser): if any(char in question.value for char in self.forbidden_chars): raise YunohostError('pattern_password_app', forbidden_chars=self.forbidden_chars) + from yunohost.utils.password import assert_password_is_strong_enough + assert_password_is_strong_enough('user', question.value) + return super(PasswordArgumentParser, self)._post_parse_value(question) diff --git a/src/yunohost/tests/test_apps_arguments_parsing.py b/src/yunohost/tests/test_apps_arguments_parsing.py index 9576ce0bf..8224c443e 100644 --- a/src/yunohost/tests/test_apps_arguments_parsing.py +++ b/src/yunohost/tests/test_apps_arguments_parsing.py @@ -247,8 +247,9 @@ def test_parse_args_in_yunohost_format_password_input_no_ask(): def test_parse_args_in_yunohost_format_password_no_input_optional(): questions = [{"name": "some_password", "type": "password", "optional": True,}] answers = {} - expected_result = OrderedDict({"some_password": ("", "password")}) - assert _parse_args_in_yunohost_format(answers, questions) == expected_result + + with pytest.raises(YunohostError): + _parse_args_in_yunohost_format(answers, questions) def test_parse_args_in_yunohost_format_password_optional_with_input(): @@ -374,6 +375,24 @@ def test_parse_args_in_yunohost_format_password_bad_chars(): _parse_args_in_yunohost_format({"some_password": i * 8}, questions) +def test_parse_args_in_yunohost_format_password_strong_enough(): + questions = [ + { + "name": "some_password", + "type": "password", + "ask": "some question", + "example": "some_value", + } + ] + + with pytest.raises(YunohostError): + # too short + _parse_args_in_yunohost_format({"some_password": "a"}, questions) + + with pytest.raises(YunohostError): + _parse_args_in_yunohost_format({"some_password": "password"}, questions) + + def test_parse_args_in_yunohost_format_path(): questions = [{"name": "some_path", "type": "path",}] answers = {"some_path": "some_value"} From db93307feaedc8f9aebf4d0021c87edef9773a85 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 Sep 2020 19:48:05 +0200 Subject: [PATCH 08/10] Update src/yunohost/app.py Co-authored-by: Kayou --- src/yunohost/app.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index a36bd1d85..3c9b9d352 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2683,6 +2683,12 @@ class UserArgumentParser(YunoHostArgumentFormatParser): question = super(UserArgumentParser, self).parse_question(question, user_answers) question.choices = user_list()["users"] + if question.default is None: + root_mail = "root@%s" % _get_maindomain() + for user in question.choices.keys(): + if root_mail in user_info(user)["mail-aliases"]: + question.default = user + break return question From 07fb33597239bc8214fece2032acab4c6f79e4e2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 Sep 2020 21:20:40 +0200 Subject: [PATCH 09/10] Missing imports --- src/yunohost/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 3c9b9d352..bd6987efd 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2679,7 +2679,8 @@ class UserArgumentParser(YunoHostArgumentFormatParser): argument_type = "user" def parse_question(self, question, user_answers): - from yunohost.user import user_list + from yunohost.user import user_list, user_info + from yunohost.domain import _get_maindomain question = super(UserArgumentParser, self).parse_question(question, user_answers) question.choices = user_list()["users"] From b9189b1979db2a640faa742ca19a545498e73941 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 9 Sep 2020 23:58:30 +0200 Subject: [PATCH 10/10] Fix tests --- locales/en.json | 1 - src/yunohost/app.py | 1 + .../tests/test_apps_arguments_parsing.py | 22 +++++++++++-------- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/locales/en.json b/locales/en.json index 67ef69922..9fb31c663 100644 --- a/locales/en.json +++ b/locales/en.json @@ -600,7 +600,6 @@ "user_unknown": "Unknown user: {user:s}", "user_update_failed": "Could not update user {user}: {error}", "user_updated": "User info changed", - "users_available": "Available users:", "yunohost_already_installed": "YunoHost is already installed", "yunohost_ca_creation_failed": "Could not create certificate authority", "yunohost_ca_creation_success": "Local certification authority created.", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index aab4f2a41..336e2a71b 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2715,6 +2715,7 @@ class AppArgumentParser(YunoHostArgumentFormatParser): class DisplayTextArgumentParser(YunoHostArgumentFormatParser): + argument_type = "display_text" def parse(self, question, user_answers): print(question["ask"]) diff --git a/src/yunohost/tests/test_apps_arguments_parsing.py b/src/yunohost/tests/test_apps_arguments_parsing.py index 79e87b7bc..dede7a0f9 100644 --- a/src/yunohost/tests/test_apps_arguments_parsing.py +++ b/src/yunohost/tests/test_apps_arguments_parsing.py @@ -883,7 +883,8 @@ def test_parse_args_in_yunohost_format_user(): expected_result = OrderedDict({"some_user": (username, "user")}) with patch.object(user, "user_list", return_value={"users": users}): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result + with patch.object(user, "user_info", return_value={}): + assert _parse_args_in_yunohost_format(answers, questions) == expected_result def test_parse_args_in_yunohost_format_user_two_users(): @@ -913,13 +914,15 @@ def test_parse_args_in_yunohost_format_user_two_users(): expected_result = OrderedDict({"some_user": (other_user, "user")}) with patch.object(user, "user_list", return_value={"users": users}): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result + with patch.object(user, "user_info", return_value={}): + assert _parse_args_in_yunohost_format(answers, questions) == expected_result answers = {"some_user": username} expected_result = OrderedDict({"some_user": (username, "user")}) with patch.object(user, "user_list", return_value={"users": users}): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result + with patch.object(user, "user_info", return_value={}): + assert _parse_args_in_yunohost_format(answers, questions) == expected_result def test_parse_args_in_yunohost_format_user_two_users_wrong_answer(): @@ -1008,13 +1011,14 @@ def test_parse_args_in_yunohost_format_user_two_users_default_input(): answers = {} with patch.object(user, "user_list", return_value={"users": users}): - expected_result = OrderedDict({"some_user": (username, "user")}) - with patch.object(msignals, "prompt", return_value=username): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result + with patch.object(user, "user_info", return_value={}): + expected_result = OrderedDict({"some_user": (username, "user")}) + with patch.object(msignals, "prompt", return_value=username): + assert _parse_args_in_yunohost_format(answers, questions) == expected_result - expected_result = OrderedDict({"some_user": (other_user, "user")}) - with patch.object(msignals, "prompt", return_value=other_user): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result + expected_result = OrderedDict({"some_user": (other_user, "user")}) + with patch.object(msignals, "prompt", return_value=other_user): + assert _parse_args_in_yunohost_format(answers, questions) == expected_result def test_parse_args_in_yunohost_format_app_empty():