From cc59501b55b269747be983aacc392f9d99aa8522 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 15 Sep 2019 16:59:34 +0200 Subject: [PATCH 01/35] Naive implementation of visitors group (without any relation to the ssowat conf yet) --- data/other/ldap_scheme.yml | 6 ++++++ locales/en.json | 3 +++ .../0011_setup_group_permission.py | 4 ++++ src/yunohost/permission.py | 13 +++++++++---- src/yunohost/user.py | 15 ++++++++++----- 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/data/other/ldap_scheme.yml b/data/other/ldap_scheme.yml index caa8fffb2..660d6fbb5 100644 --- a/data/other/ldap_scheme.yml +++ b/data/other/ldap_scheme.yml @@ -57,6 +57,12 @@ children: objectClass: - posixGroup - groupOfNamesYnh + cn=visitors,ou=groups: + cn: visitors + gidNumber: "4003" + objectClass: + - posixGroup + - groupOfNamesYnh depends_children: cn=mail.main,ou=permission: diff --git a/locales/en.json b/locales/en.json index ae349edf3..5df21b684 100644 --- a/locales/en.json +++ b/locales/en.json @@ -230,6 +230,9 @@ "group_already_exist_on_system": "Group {group} already exists in the system group", "group_created": "Group '{group}' successfully created", "group_creation_failed": "Failed to create group {group}: {error}", + "group_cannot_edit_all_users": "The group 'all_users' cannot be edited manually. It is a special group meant to contain all users registered in Yunohost", + "group_cannot_edit_visitors": "The group 'visitors' cannot be edited manually. It is a special group representing anonymous visitors", + "group_cannot_edit_primary_group": "The group '{group}' cannot be edited manually. It is the primary group meant to contain only one specific user.", "group_cannot_be_edited": "The group {group} cannot be edited manually.", "group_cannot_be_deleted": "The group {group} cannot be deleted manually.", "group_deleted": "Group '{group}' deleted", diff --git a/src/yunohost/data_migrations/0011_setup_group_permission.py b/src/yunohost/data_migrations/0011_setup_group_permission.py index 8949239e0..b3e11cb14 100644 --- a/src/yunohost/data_migrations/0011_setup_group_permission.py +++ b/src/yunohost/data_migrations/0011_setup_group_permission.py @@ -63,6 +63,7 @@ class MyMigration(Migration): self.remove_if_exists("cn=sftpusers,ou=groups") self.remove_if_exists("ou=permission") self.remove_if_exists('cn=all_users,ou=groups') + self.remove_if_exists('cn=visitors,ou=groups') attr_dict = ldap_map['parents']['ou=permission'] ldap.add('ou=permission', attr_dict) @@ -70,6 +71,9 @@ class MyMigration(Migration): attr_dict = ldap_map['children']['cn=all_users,ou=groups'] ldap.add('cn=all_users,ou=groups', attr_dict) + attr_dict = ldap_map['children']['cn=visitors,ou=groups'] + ldap.add('cn=visitors,ou=groups', attr_dict) + for rdn, attr_dict in ldap_map['depends_children'].items(): ldap.add(rdn, attr_dict) except Exception as e: diff --git a/src/yunohost/permission.py b/src/yunohost/permission.py index 1472f4b88..dbfc6e6f5 100644 --- a/src/yunohost/permission.py +++ b/src/yunohost/permission.py @@ -142,10 +142,15 @@ def user_permission_update(operation_logger, permission, add=None, remove=None, # we shall warn the users that they should probably choose between one or the other, # because the current situation is probably not what they expect / is temporary ? - if len(new_allowed_groups) > 1 and "all_users" in new_allowed_groups: - # FIXME : i18n - # FIXME : write a better explanation ? - logger.warning("This permission is currently enabled for all users in addition to other groups. You probably want to either remove the 'all_users' permission or remove the specific groups currently allowed.") + if len(new_allowed_groups) > 1: + if "all_users" in new_allowed_groups: + # FIXME : i18n + # FIXME : write a better explanation ? + logger.warning("This permission is currently enabled for all users in addition to other groups. You probably want to either remove the 'all_users' permission or remove the other groups currently allowed.") + if "visitors" in new_allowed_groups: + # FIXME : i18n + # FIXME : write a better explanation ? + logger.warning("This permission is currently enabled for visitors in addition to other groups. You probably want to either remove the 'visitors' permission or remove the other groups currently allowed.") # Don't update LDAP if we update exactly the same values if set(new_allowed_groups) == set(current_allowed_groups): diff --git a/src/yunohost/user.py b/src/yunohost/user.py index c6413d7e1..581354f77 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -635,7 +635,7 @@ def user_group_delete(operation_logger, groupname, force=False, sync_perm=True): # # We also can't delete "all_users" because that's a special group... existing_users = user_list()['users'].keys() - undeletable_groups = existing_users + ["all_users", "admins"] + undeletable_groups = existing_users + ["all_users", "visitors"] if groupname in undeletable_groups and not force: raise YunohostError('group_cannot_be_deleted', group=groupname) @@ -670,13 +670,18 @@ def user_group_update(operation_logger, groupname, add=None, remove=None, force= from yunohost.permission import permission_sync_to_user from yunohost.utils.ldap import _get_ldap_interface + existing_users = user_list()['users'].keys() + # Refuse to edit a primary group of a user (e.g. group 'sam' related to user 'sam') # Those kind of group should only ever contain the user (e.g. sam) and only this one. # We also can't edit "all_users" without the force option because that's a special group... - existing_users = user_list()['users'].keys() - uneditable_groups = existing_users + ["all_users", "admins"] - if groupname in uneditable_groups and not force: - raise YunohostError('group_cannot_be_edited', group=groupname) + if not force: + if groupname == "all_users": + raise YunohostError('group_cannot_edit_all_users') + elif groupname == "all_users": + raise YunohostError('group_cannot_edit_visitors') + elif groupname in existing_users: + raise YunohostError('group_cannot_edit_primary_group', group=groupname) # We extract the uid for each member of the group to keep a simple flat list of members current_group = user_group_info(groupname)["members"] From 95a8dfa71c22103152a9241bc508de96f04cfe19 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 15 Sep 2019 16:59:44 +0200 Subject: [PATCH 02/35] Simplify part of app_ssowatconf --- src/yunohost/app.py | 49 ++++++++++++++++++++------------------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index ab290cb4d..a9c91aaf5 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -41,7 +41,7 @@ from datetime import datetime from moulinette import msignals, m18n, msettings from moulinette.utils.log import getActionLogger -from moulinette.utils.filesystem import read_json, read_toml +from moulinette.utils.filesystem import read_json, read_toml, read_yaml from yunohost.service import service_log, service_status, _run_service_command from yunohost.utils import packages @@ -1366,34 +1366,29 @@ def app_ssowatconf(): return s.split(',') if s else [] for app in apps_list: - with open(APPS_SETTING_PATH + app['id'] + '/settings.yml') as f: - app_settings = yaml.load(f) - if 'no_sso' in app_settings: - continue + app_settings = read_yaml(APPS_SETTING_PATH + app['id'] + '/settings.yml') - for item in _get_setting(app_settings, 'skipped_uris'): - if item[-1:] == '/': - item = item[:-1] - skipped_urls.append(app_settings['domain'] + app_settings['path'].rstrip('/') + item) - for item in _get_setting(app_settings, 'skipped_regex'): - skipped_regex.append(item) - for item in _get_setting(app_settings, 'unprotected_uris'): - if item[-1:] == '/': - item = item[:-1] - unprotected_urls.append(app_settings['domain'] + app_settings['path'].rstrip('/') + item) - for item in _get_setting(app_settings, 'unprotected_regex'): - unprotected_regex.append(item) - for item in _get_setting(app_settings, 'protected_uris'): - if item[-1:] == '/': - item = item[:-1] - protected_urls.append(app_settings['domain'] + app_settings['path'].rstrip('/') + item) - for item in _get_setting(app_settings, 'protected_regex'): - protected_regex.append(item) - if 'redirected_urls' in app_settings: - redirected_urls.update(app_settings['redirected_urls']) - if 'redirected_regex' in app_settings: - redirected_regex.update(app_settings['redirected_regex']) + if 'no_sso' in app_settings: + continue + + app_root_webpath = app_settings['domain'] + app_settings['path'].rstrip('/') + + # Skipped + skipped_urls += [app_root_webpath + uri.rstrip("/") for uri in _get_setting(app_settings, 'skipped_uris')] + skipped_regex += _get_setting(app_settings, 'skipped_regex') + + # Unprotected + unprotected_urls += [app_root_webpath + uri.rstrip("/") for uri in _get_setting(app_settings, 'unprotected_uris')] + unprotected_regex += _get_setting(app_settings, 'unprotected_regex') + + # Protected + unprotected_urls += [app_root_webpath + uri.rstrip("/") for uri in _get_setting(app_settings, 'protected_uris')] + unprotected_regex += _get_setting(app_settings, 'protected_regex') + + # Redirected + redirected_urls.update(app_settings.get('redirected_urls', {})) + redirected_regex.update(app_settings.get('redirected_regex', {})) for domain in domains: skipped_urls.extend([domain + '/yunohost/admin', domain + '/yunohost/api']) From 8abfd2a6e60bfd53d6fb398261e002e223a0c217 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 15 Sep 2019 17:58:41 +0200 Subject: [PATCH 03/35] Naive implementation of protected/unprotected inplementation using the visitors group --- src/yunohost/app.py | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index a9c91aaf5..52371ff29 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1345,6 +1345,7 @@ def app_ssowatconf(): main_domain = _get_maindomain() domains = domain_list()['domains'] + all_permissions = user_permission_list(full=True)['permissions'] skipped_urls = [] skipped_regex = [] @@ -1378,18 +1379,32 @@ def app_ssowatconf(): skipped_urls += [app_root_webpath + uri.rstrip("/") for uri in _get_setting(app_settings, 'skipped_uris')] skipped_regex += _get_setting(app_settings, 'skipped_regex') - # Unprotected - unprotected_urls += [app_root_webpath + uri.rstrip("/") for uri in _get_setting(app_settings, 'unprotected_uris')] - unprotected_regex += _get_setting(app_settings, 'unprotected_regex') - - # Protected - unprotected_urls += [app_root_webpath + uri.rstrip("/") for uri in _get_setting(app_settings, 'protected_uris')] - unprotected_regex += _get_setting(app_settings, 'protected_regex') - # Redirected redirected_urls.update(app_settings.get('redirected_urls', {})) redirected_regex.update(app_settings.get('redirected_regex', {})) + # Legacy permission system using (un)protected_uris and _regex managed in app settings... + unprotected_urls += [app_root_webpath + uri.rstrip("/") for uri in _get_setting(app_settings, 'unprotected_uris')] + unprotected_urls += [app_root_webpath + uri.rstrip("/") for uri in _get_setting(app_settings, 'protected_uris')] + unprotected_regex += _get_setting(app_settings, 'unprotected_regex') + unprotected_regex += _get_setting(app_settings, 'protected_regex') + + # New permission system + this_app_perms = {name: info for name, info in all_permissions.items if name.startswith(app + ".")} + for perm_name, perm_info in this_app_perms: + urls = [url.rstrip("/") for url in perm_info["urls"]] + if "visitors" in perm_info["allowed"]: + unprotected_urls += urls + + # Legacy stuff : we remove now unprotected-urls that might have been declared as protected earlier... + protected_urls = [u for u in protected_urls if u not in urls] + else: + # TODO : small optimization to implement : we don't need to explictly add all the app roots + protected_urls += urls + + # Legacy stuff : we remove now unprotected-urls that might have been declared as protected earlier... + unprotected_urls = [u for u in unprotected_urls if u not in urls] + for domain in domains: skipped_urls.extend([domain + '/yunohost/admin', domain + '/yunohost/api']) @@ -1397,8 +1412,10 @@ def app_ssowatconf(): skipped_regex.append("^[^/]*/%.well%-known/acme%-challenge/.*$") skipped_regex.append("^[^/]*/%.well%-known/autoconfig/mail/config%-v1%.1%.xml.*$") + + permissions_per_url = {} - for permission_name, permission_infos in user_permission_list(full=True)['permissions'].items(): + for permission_name, permission_infos in all_permissions.items(): for url in permission_infos["urls"]: permissions_per_url[url] = permission_infos['corresponding_users'] From c4743398e687738a8b55bd500f219f7a69afe28e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 15 Sep 2019 18:10:58 +0200 Subject: [PATCH 04/35] Deprecate (un)protected_uris and _regex settings + more explicit deprecation warning for app_add/remove/clearaccess --- data/helpers.d/setting | 4 +++- src/yunohost/app.py | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/data/helpers.d/setting b/data/helpers.d/setting index 502da1ed7..d083ed563 100644 --- a/data/helpers.d/setting +++ b/data/helpers.d/setting @@ -176,6 +176,8 @@ else: elif action == "set": if key in ['redirected_urls', 'redirected_regex']: value = yaml.load(value) + if key in ["unprotected_uris", "unprotected_regex", "protected_uris", "protected_regex"]: + logger.warning("/!\\ Packagers ! This app is using the legacy permission system. Please delete these legacy settings and use the new helpers ynh_permission_{create,urls,update,delete} and the 'visitors' group to manage public/private access.") settings[key] = value else: raise ValueError("action should either be get, set or delete") @@ -249,7 +251,7 @@ ynh_permission_create() { # Remove a permission for the app (note that when the app is removed all permission is automatically removed) # -# usage: ynh_permission_remove --permission "permission" +# usage: ynh_permission_delete --permission "permission" # | arg: permission - the name for the permission (by default a permission named "main" is removed automatically when the app is removed) # # example: ynh_permission_delete --permission editors diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 52371ff29..8b0c99d46 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1037,6 +1037,8 @@ def app_addaccess(apps, users=[]): """ from yunohost.permission import user_permission_update + logger.warning("/!\\ Packagers ! This app is using the legacy permission system. Please use the new helpers ynh_permission_{create,urls,update,delete} and the 'visitors' group to manage permissions.") + output = {} for app in apps: permission = user_permission_update(app+".main", add=users) @@ -1056,6 +1058,8 @@ def app_removeaccess(apps, users=[]): """ from yunohost.permission import user_permission_update + logger.warning("/!\\ Packagers ! This app is using the legacy permission system. Please use the new helpers ynh_permission_{create,urls,update,delete} and the 'visitors' group to manage permissions.") + output = {} for app in apps: permission = user_permission_update(app+".main", remove=users) @@ -1074,6 +1078,8 @@ def app_clearaccess(apps): """ from yunohost.permission import user_permission_reset + logger.warning("/!\\ Packagers ! This app is using the legacy permission system. Please use the new helpers ynh_permission_{create,urls,update,delete} and the 'visitors' group to manage permissions.") + output = {} for app in apps: permission = user_permission_reset(app+".main") @@ -1181,6 +1187,8 @@ def app_setting(app, key, value=None, delete=False): # FIXME: Allow multiple values for some keys? if key in ['redirected_urls', 'redirected_regex']: value = yaml.load(value) + if key in ["unprotected_uris", "unprotected_regex", "protected_uris", "protected_regex"]: + logger.warning("/!\ Packagers ! This app is using the legacy permission system. Please delete these legacy settings and use the new helpers ynh_permission_{create,urls,update,delete} and the 'visitors' group to manage public/private access.") app_settings[key] = value _set_app_settings(app, app_settings) From b2a26a64a74d2f7289d857b7bb780ac2f91741ce Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 15 Sep 2019 18:33:31 +0200 Subject: [PATCH 05/35] Naively migrate legacy and classical unprotected_uris = / that sets the app as public --- src/yunohost/app.py | 8 +++++++- .../data_migrations/0011_setup_group_permission.py | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 8b0c99d46..537616e68 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -733,7 +733,7 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback from yunohost.log import OperationLogger - from yunohost.permission import user_permission_list, permission_create, permission_urls, permission_delete, permission_sync_to_user + from yunohost.permission import user_permission_list, permission_create, permission_urls, permission_delete, permission_sync_to_user, user_permission_update # Fetch or extract sources if not os.path.exists(INSTALL_TMP): @@ -952,7 +952,13 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu domain = app_settings.get('domain', None) path = app_settings.get('path', None) if domain and path: + # FIXME : might want to move this to before running the install script because some app need to run install script during initialization etc (idk) ? permission_urls(app_instance_name+".main", add=[domain+path], sync_perm=False) + + # Migrate classic public app still using the legacy unprotected_uris + if app_settings.get("unprotected_uris", None) == "/": + user_permission_update(app_instance_name+".main", remove="all_users", add="visitors", sync_perm=False) + permission_sync_to_user() logger.success(m18n.n('installation_complete')) diff --git a/src/yunohost/data_migrations/0011_setup_group_permission.py b/src/yunohost/data_migrations/0011_setup_group_permission.py index b3e11cb14..c79d80e0c 100644 --- a/src/yunohost/data_migrations/0011_setup_group_permission.py +++ b/src/yunohost/data_migrations/0011_setup_group_permission.py @@ -113,6 +113,12 @@ class MyMigration(Migration): user_permission_update(app+".main", remove="all_users", add=allowed_group, sync_perm=False) app_setting(app, 'allowed_users', delete=True) + # Migrate classic public app still using the legacy unprotected_uris + if app_setting(app, "unprotected_uris") == "/": + user_permission_update(app+".main", remove="all_users", add="visitors", sync_perm=False) + + permission_sync_to_user() + def run(self): # FIXME : what do we really want to do here ... From 821a3ac4ff0f3180ff5b5884f020c02b3a982b34 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 15 Sep 2019 18:53:25 +0200 Subject: [PATCH 06/35] Draft tests to check that permissions are actually propagated and effective on the SSO --- src/yunohost/tests/test_permission.py | 55 ++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/src/yunohost/tests/test_permission.py b/src/yunohost/tests/test_permission.py index 94728505d..1c81e015f 100644 --- a/src/yunohost/tests/test_permission.py +++ b/src/yunohost/tests/test_permission.py @@ -333,7 +333,7 @@ def test_permission_remove_url_not_added(): def test_permission_app_install(): app_install("./tests/apps/permissions_app_ynh", - args="domain=%s&path=%s&admin=%s" % (maindomain, "/urlpermissionapp", "alice"), force=True) + args="domain=%s&path=%s&is_public=0&admin=%s" % (maindomain, "/urlpermissionapp", "alice"), force=True) res = user_permission_list(full=True)['permissions'] assert "permissions_app.main" in res @@ -361,7 +361,7 @@ def test_permission_app_install(): def test_permission_app_remove(): app_install("./tests/apps/permissions_app_ynh", - args="domain=%s&path=%s&admin=%s" % (maindomain, "/urlpermissionapp", "alice"), force=True) + args="domain=%s&path=%s&is_public=0&admin=%s" % (maindomain, "/urlpermissionapp", "alice"), force=True) app_remove("permissions_app") # Check all permissions for this app got deleted @@ -383,3 +383,54 @@ def test_permission_app_change_url(): assert res['permissions_app.main']['urls'] == [maindomain + "/newchangeurl"] assert res['permissions_app.admin']['urls'] == [maindomain + "/newchangeurl/admin"] assert res['permissions_app.dev']['urls'] == [maindomain + "/newchangeurl/dev"] + + +def test_permission_app_propagation_on_ssowat(): + + # TODO / FIXME : To be actually implemented later .... + raise NotImplementedError + + app_install("./tests/apps/permissions_app_ynh", + args="domain=%s&path=%s&is_public=1&admin=%s" % (maindomain, "/urlpermissionapp", "alice"), force=True) + + res = user_permission_list(full=True)['permissions'] + assert res['permissions_app.main']['allowed'] == ["all_users"] + + assert can_access(maindomain + "/urlpermissionapp", logged_as=None) + assert can_access(maindomain + "/urlpermissionapp", logged_as="alice") + + user_permission_update("permissions_app.main", remove="visitors", add="bob") + res = user_permission_list(full=True)['permissions'] + + assert cannot_access(maindomain + "/urlpermissionapp", logged_as=None) + assert cannot_access(maindomain + "/urlpermissionapp", logged_as="alice") + assert can_access(maindomain + "/urlpermissionapp", logged_as="bob") + + # Test admin access, as configured during install, only alice should be able to access it + + assert cannot_access(maindomain + "/urlpermissionapp/admin", logged_as=None) + assert cannot_access(maindomain + "/urlpermissionapp/admin", logged_as="alice") + assert can_access(maindomain + "/urlpermissionapp/admin", logged_as="bob") + +def test_permission_legacy_app_propagation_on_ssowat(): + + # TODO / FIXME : To be actually implemented later .... + raise NotImplementedError + + app_install("./tests/apps/legacy_app_ynh", + args="domain=%s&path=%s" % (maindomain, "/legacy"), force=True) + + # App is configured as public by default using the legacy unprotected_uri mechanics + # It should automatically be migrated during the install + assert res['permissions_app.main']['allowed'] == ["visitors"] + + assert can_access(maindomain + "/legacy", logged_as=None) + assert can_access(maindomain + "/legacy", logged_as="alice") + + # Try to update the permission and check that permissions are still consistent + user_permission_update("legacy_app.main", remove="visitors", add="bob") + res = user_permission_list(full=True)['permissions'] + + assert cannot_access(maindomain + "/legacy", logged_as=None) + assert cannot_access(maindomain + "/legacy", logged_as="alice") + assert can_access(maindomain + "/legacy", logged_as="bob") From 64e388fa7d952690c3f35d8b13f67d52869bb383 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 17 Sep 2019 23:38:17 +0200 Subject: [PATCH 07/35] Implement helper function to test if we're able to access a webpage being logged in (or not) as user --- src/yunohost/tests/test_permission.py | 56 ++++++++++++++++++++------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/src/yunohost/tests/test_permission.py b/src/yunohost/tests/test_permission.py index 1c81e015f..51bf6a4c6 100644 --- a/src/yunohost/tests/test_permission.py +++ b/src/yunohost/tests/test_permission.py @@ -1,3 +1,4 @@ +import requests import pytest from yunohost.app import app_install, app_remove, app_change_url, app_list, app_map @@ -11,6 +12,7 @@ from yunohost.utils.error import YunohostError # Get main domain maindomain = _get_maindomain() +dummy_password = "test123Ynh" def clean_user_groups_permission(): for u in user_list()['users']: @@ -27,8 +29,8 @@ def clean_user_groups_permission(): def setup_function(function): clean_user_groups_permission() - user_create("alice", "Alice", "White", "alice@" + maindomain, "test123Ynh") - user_create("bob", "Bob", "Snow", "bob@" + maindomain, "test123Ynh") + user_create("alice", "Alice", "White", "alice@" + maindomain, dummy_password) + user_create("bob", "Bob", "Snow", "bob@" + maindomain, dummy_password) permission_create("wiki.main", urls=[maindomain + "/wiki"], sync_perm=False) permission_create("blog.main", sync_perm=False) user_permission_update("blog.main", remove="all_users", add="alice") @@ -156,6 +158,30 @@ def check_permission_for_apps(): assert installed_apps == app_perms_prefix + +def can_access_webpage(webpath, logged_as=None): + + webpath = webpath.rstrip("/") + webroot = webpath.rsplit("/", 1)[0] + sso_url = webroot+"/yunohost/sso" + + # Anonymous access + if not logged_as: + r = requests.get(webpath, verify=False) + # Login as a user using dummy password + else: + with requests.Session() as session: + session.post(sso_url, + data={"user": logged_as, + "password": dummy_password}, + headers={"Referer": sso_url, + "Content-Type": "application/x-www-form-urlencoded"}, + verify=False) + r = session.get(webpath, verify=False) + + # If we can't access it, we got redirected to the sso + return not r.url.startswith(sso_url) + # # List functions # @@ -396,21 +422,21 @@ def test_permission_app_propagation_on_ssowat(): res = user_permission_list(full=True)['permissions'] assert res['permissions_app.main']['allowed'] == ["all_users"] - assert can_access(maindomain + "/urlpermissionapp", logged_as=None) - assert can_access(maindomain + "/urlpermissionapp", logged_as="alice") + assert can_access_webpage(maindomain + "/urlpermissionapp", logged_as=None) + assert can_access_webpage(maindomain + "/urlpermissionapp", logged_as="alice") user_permission_update("permissions_app.main", remove="visitors", add="bob") res = user_permission_list(full=True)['permissions'] - assert cannot_access(maindomain + "/urlpermissionapp", logged_as=None) - assert cannot_access(maindomain + "/urlpermissionapp", logged_as="alice") - assert can_access(maindomain + "/urlpermissionapp", logged_as="bob") + assert not can_access_webpage(maindomain + "/urlpermissionapp", logged_as=None) + assert not can_access_webpage(maindomain + "/urlpermissionapp", logged_as="alice") + assert can_access_webpage(maindomain + "/urlpermissionapp", logged_as="bob") # Test admin access, as configured during install, only alice should be able to access it - assert cannot_access(maindomain + "/urlpermissionapp/admin", logged_as=None) - assert cannot_access(maindomain + "/urlpermissionapp/admin", logged_as="alice") - assert can_access(maindomain + "/urlpermissionapp/admin", logged_as="bob") + assert not can_access_webpage(maindomain + "/urlpermissionapp/admin", logged_as=None) + assert not can_access_webpage(maindomain + "/urlpermissionapp/admin", logged_as="alice") + assert can_access_webpage(maindomain + "/urlpermissionapp/admin", logged_as="bob") def test_permission_legacy_app_propagation_on_ssowat(): @@ -424,13 +450,13 @@ def test_permission_legacy_app_propagation_on_ssowat(): # It should automatically be migrated during the install assert res['permissions_app.main']['allowed'] == ["visitors"] - assert can_access(maindomain + "/legacy", logged_as=None) - assert can_access(maindomain + "/legacy", logged_as="alice") + assert can_access_webpage(maindomain + "/legacy", logged_as=None) + assert can_access_webpage(maindomain + "/legacy", logged_as="alice") # Try to update the permission and check that permissions are still consistent user_permission_update("legacy_app.main", remove="visitors", add="bob") res = user_permission_list(full=True)['permissions'] - assert cannot_access(maindomain + "/legacy", logged_as=None) - assert cannot_access(maindomain + "/legacy", logged_as="alice") - assert can_access(maindomain + "/legacy", logged_as="bob") + assert not can_access_webpage(maindomain + "/legacy", logged_as=None) + assert not can_access_webpage(maindomain + "/legacy", logged_as="alice") + assert can_access_webpage(maindomain + "/legacy", logged_as="bob") From 00795a7a0156d5d45aeabb63819ab3d6511270e5 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 18 Sep 2019 18:38:47 +0200 Subject: [PATCH 08/35] Make migration re-run even more robust --- src/yunohost/data_migrations/0011_setup_group_permission.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/yunohost/data_migrations/0011_setup_group_permission.py b/src/yunohost/data_migrations/0011_setup_group_permission.py index c79d80e0c..a99dfb7c1 100644 --- a/src/yunohost/data_migrations/0011_setup_group_permission.py +++ b/src/yunohost/data_migrations/0011_setup_group_permission.py @@ -62,12 +62,14 @@ class MyMigration(Migration): try: self.remove_if_exists("cn=sftpusers,ou=groups") self.remove_if_exists("ou=permission") - self.remove_if_exists('cn=all_users,ou=groups') - self.remove_if_exists('cn=visitors,ou=groups') + self.remove_if_exists('ou=groups') attr_dict = ldap_map['parents']['ou=permission'] ldap.add('ou=permission', attr_dict) + attr_dict = ldap_map['parents']['ou=groups'] + ldap.add('ou=groups', attr_dict) + attr_dict = ldap_map['children']['cn=all_users,ou=groups'] ldap.add('cn=all_users,ou=groups', attr_dict) From 8d01a816f3cd0fd07f570cd2d9f290d01f100ed9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 18 Sep 2019 18:39:05 +0200 Subject: [PATCH 09/35] Typo fixes following tests --- src/yunohost/app.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 537616e68..7938d6786 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1404,8 +1404,8 @@ def app_ssowatconf(): unprotected_regex += _get_setting(app_settings, 'protected_regex') # New permission system - this_app_perms = {name: info for name, info in all_permissions.items if name.startswith(app + ".")} - for perm_name, perm_info in this_app_perms: + this_app_perms = {name: info for name, info in all_permissions.items() if name.startswith(app['id'] + ".")} + for perm_name, perm_info in this_app_perms.items(): urls = [url.rstrip("/") for url in perm_info["urls"]] if "visitors" in perm_info["allowed"]: unprotected_urls += urls @@ -1414,6 +1414,7 @@ def app_ssowatconf(): protected_urls = [u for u in protected_urls if u not in urls] else: # TODO : small optimization to implement : we don't need to explictly add all the app roots + protected_urls += urls # Legacy stuff : we remove now unprotected-urls that might have been declared as protected earlier... From 87050276b4a217acea21b7f45820bdfaead1194d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 19 Sep 2019 19:26:41 +0200 Subject: [PATCH 10/35] Finish to implement first visitor test + fixes following test ... --- src/yunohost/app.py | 12 +++++------ src/yunohost/tests/test_permission.py | 29 +++++++++++++-------------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 7938d6786..bba5fb104 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -429,8 +429,10 @@ def app_map(app=None, raw=False, user=None): continue if 'no_sso' in app_settings: # I don't think we need to check for the value here continue - if user and user not in permissions[app_id + ".main"]["corresponding_users"]: - continue + if user: + main_perm = permissions[app_id + ".main"] + if user not in main_perm["corresponding_users"] and "visitors" not in main_perm["allowed"]: + continue domain = app_settings['domain'] path = app_settings['path'] @@ -2613,10 +2615,8 @@ def _parse_args_in_yunohost_format(args, action_args): if arg_value not in domain_list()['domains']: raise YunohostError('app_argument_invalid', name=arg_name, error=m18n.n('domain_unknown')) elif arg_type == 'user': - try: - user_info(arg_value) - except YunohostError as e: - raise YunohostError('app_argument_invalid', name=arg_name, error=e) + if not arg_value in user_list()["users"].keys(): + raise YunohostError('app_argument_invalid', name=arg_name, error=m18n.n('user_unknown', user=arg_value)) elif arg_type == 'app': if not _is_installed(arg_value): raise YunohostError('app_argument_invalid', name=arg_name, error=m18n.n('app_unknown')) diff --git a/src/yunohost/tests/test_permission.py b/src/yunohost/tests/test_permission.py index 51bf6a4c6..bef042be1 100644 --- a/src/yunohost/tests/test_permission.py +++ b/src/yunohost/tests/test_permission.py @@ -19,7 +19,7 @@ def clean_user_groups_permission(): user_delete(u) for g in user_group_list()['groups']: - if g != "all_users": + if g not in ["all_users", "visitors"]: user_group_delete(g) for p in user_permission_list()['permissions']: @@ -162,8 +162,7 @@ def check_permission_for_apps(): def can_access_webpage(webpath, logged_as=None): webpath = webpath.rstrip("/") - webroot = webpath.rsplit("/", 1)[0] - sso_url = webroot+"/yunohost/sso" + sso_url = "https://"+maindomain+"/yunohost/sso/" # Anonymous access if not logged_as: @@ -177,6 +176,8 @@ def can_access_webpage(webpath, logged_as=None): headers={"Referer": sso_url, "Content-Type": "application/x-www-form-urlencoded"}, verify=False) + # We should have some cookies related to authentication now + assert session.cookies r = session.get(webpath, verify=False) # If we can't access it, we got redirected to the sso @@ -413,30 +414,28 @@ def test_permission_app_change_url(): def test_permission_app_propagation_on_ssowat(): - # TODO / FIXME : To be actually implemented later .... - raise NotImplementedError - app_install("./tests/apps/permissions_app_ynh", args="domain=%s&path=%s&is_public=1&admin=%s" % (maindomain, "/urlpermissionapp", "alice"), force=True) res = user_permission_list(full=True)['permissions'] - assert res['permissions_app.main']['allowed'] == ["all_users"] + assert res['permissions_app.main']['allowed'] == ["visitors"] - assert can_access_webpage(maindomain + "/urlpermissionapp", logged_as=None) - assert can_access_webpage(maindomain + "/urlpermissionapp", logged_as="alice") + app_webroot = "https://%s/urlpermissionapp" % maindomain + assert can_access_webpage(app_webroot, logged_as=None) + assert can_access_webpage(app_webroot, logged_as="alice") user_permission_update("permissions_app.main", remove="visitors", add="bob") res = user_permission_list(full=True)['permissions'] - assert not can_access_webpage(maindomain + "/urlpermissionapp", logged_as=None) - assert not can_access_webpage(maindomain + "/urlpermissionapp", logged_as="alice") - assert can_access_webpage(maindomain + "/urlpermissionapp", logged_as="bob") + assert not can_access_webpage(app_webroot, logged_as=None) + assert not can_access_webpage(app_webroot, logged_as="alice") + assert can_access_webpage(app_webroot, logged_as="bob") # Test admin access, as configured during install, only alice should be able to access it - assert not can_access_webpage(maindomain + "/urlpermissionapp/admin", logged_as=None) - assert not can_access_webpage(maindomain + "/urlpermissionapp/admin", logged_as="alice") - assert can_access_webpage(maindomain + "/urlpermissionapp/admin", logged_as="bob") + assert not can_access_webpage(app_webroot+"/admin", logged_as=None) + assert can_access_webpage(app_webroot+"/admin", logged_as="alice") + assert not can_access_webpage(app_webroot+"/admin", logged_as="bob") def test_permission_legacy_app_propagation_on_ssowat(): From ebf2fb9a141da65954d1855bc025f0cf362c6f99 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 20 Sep 2019 20:13:51 +0200 Subject: [PATCH 11/35] Use relative urls by default for permissions while still supporting absolute urls ... --- src/yunohost/app.py | 18 +++++------- .../0011_setup_group_permission.py | 2 +- src/yunohost/permission.py | 29 ++++++++++--------- 3 files changed, 23 insertions(+), 26 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index bba5fb104..231568439 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -553,8 +553,6 @@ def app_change_url(operation_logger, app, domain, path): app_setting(app, 'domain', value=domain) app_setting(app, 'path', value=path) - permission_urls(app+".main", add=[domain+path], remove=[old_domain+old_path], sync_perm=True) - # avoid common mistakes if _run_service_command("reload", "nginx") is False: # grab nginx errors @@ -868,10 +866,9 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu if os.path.exists(os.path.join(extracted_app_folder, file_to_copy)): os.system('cp -R %s/%s %s' % (extracted_app_folder, file_to_copy, app_setting_path)) - # Create permission before the install (useful if the install script redefine the permission) - # Note that sync_perm is disabled to avoid triggering a whole bunch of code and messages - # can't be sure that we don't have one case when it's needed - permission_create(app_instance_name+".main", sync_perm=False) + # Initialize the main permission for the app + # After the install, if apps don't have a domain and path defined, the default url '/' is removed from the permission + permission_create(app_instance_name+".main", urls=["/"]) # Execute the app install script install_retcode = 1 @@ -949,17 +946,16 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu os.system('chown -R root: %s' % app_setting_path) os.system('chown -R admin: %s/scripts' % app_setting_path) - # Add path in permission if it's defined in the app install script + # If an app doesn't have at least a domain and a path, assume it's not a webapp and remove the default "/" permission app_settings = _get_app_settings(app_instance_name) domain = app_settings.get('domain', None) path = app_settings.get('path', None) - if domain and path: - # FIXME : might want to move this to before running the install script because some app need to run install script during initialization etc (idk) ? - permission_urls(app_instance_name+".main", add=[domain+path], sync_perm=False) + if not (domain and path): + permission_urls(app_instance_name + ".main", remove=["/"], sync_perm=False) # Migrate classic public app still using the legacy unprotected_uris if app_settings.get("unprotected_uris", None) == "/": - user_permission_update(app_instance_name+".main", remove="all_users", add="visitors", sync_perm=False) + user_permission_update(app_instance_name + ".main", remove="all_users", add="visitors", sync_perm=False) permission_sync_to_user() diff --git a/src/yunohost/data_migrations/0011_setup_group_permission.py b/src/yunohost/data_migrations/0011_setup_group_permission.py index a99dfb7c1..dd5b3c274 100644 --- a/src/yunohost/data_migrations/0011_setup_group_permission.py +++ b/src/yunohost/data_migrations/0011_setup_group_permission.py @@ -108,7 +108,7 @@ class MyMigration(Migration): path = app_setting(app, 'path') domain = app_setting(app, 'domain') - urls = [domain + path] if domain and path else None + urls = "/" if domain and path else None permission_create(app+".main", urls=urls, sync_perm=False) if permission: allowed_group = permission.split(',') diff --git a/src/yunohost/permission.py b/src/yunohost/permission.py index dbfc6e6f5..5f9a88e11 100644 --- a/src/yunohost/permission.py +++ b/src/yunohost/permission.py @@ -268,7 +268,18 @@ def permission_create(operation_logger, permission, urls=None, sync_perm=True): Keyword argument: permission -- Name of the permission (e.g. mail or nextcloud or wordpress.editors) - urls -- list of urls to specify for the permission + urls -- list of urls to specify for the permission. + + Urls are assumed to be relative to the app domain/path if they start with '/'. + For example: + / -> domain.tld/app + /admin -> domain.tld/app/admin + domain.tld/app/api -> domain.tld/app/api + + Urls can be later treated as regexes when they start with "re:". + For example: + re:/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ + re:domain.tld/app/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ """ from yunohost.utils.ldap import _get_ldap_interface @@ -302,7 +313,7 @@ def permission_create(operation_logger, permission, urls=None, sync_perm=True): attr_dict['groupPermission'] = ['cn=all_users,ou=groups,dc=yunohost,dc=org'] if urls: - attr_dict['URL'] = [_normalize_url(url) for url in urls] + attr_dict['URL'] = urls operation_logger.related_to.append(('app', permission.split(".")[0])) operation_logger.start() @@ -326,8 +337,8 @@ def permission_urls(operation_logger, permission, add=None, remove=None, sync_pe Keyword argument: permission -- Name of the permission (e.g. mail or nextcloud or wordpress.editors) - add -- List of urls to add - remove -- List of urls to remove + add -- List of urls to add (c.f. permission_create for documentation about their format) + remove -- List of urls to remove (c.f. permission_create for documentation about their format) """ from yunohost.utils.ldap import _get_ldap_interface @@ -345,11 +356,9 @@ def permission_urls(operation_logger, permission, add=None, remove=None, sync_pe if add: urls_to_add = [add] if not isinstance(add, list) else add - urls_to_add = [_normalize_url(url) for url in urls_to_add] new_urls += urls_to_add if remove: urls_to_remove = [remove] if not isinstance(remove, list) else remove - urls_to_remove = [_normalize_url(url) for url in urls_to_remove] new_urls = [u for u in new_urls if u not in urls_to_remove] if set(new_urls) == set(existing_permission["urls"]): @@ -457,11 +466,3 @@ def permission_sync_to_user(): # Reload unscd, otherwise the group ain't propagated to the LDAP database os.system('nscd --invalidate=passwd') os.system('nscd --invalidate=group') - - -def _normalize_url(url): - from yunohost.domain import _normalize_domain_path - domain = url[:url.index('/')] - path = url[url.index('/'):] - domain, path = _normalize_domain_path(domain, path) - return domain + path From 2b51d247fb5a1a66c152c13a6ad83e97fdaceee3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 20 Sep 2019 20:14:14 +0200 Subject: [PATCH 12/35] Propagate changes on app helpers + tests --- data/helpers.d/setting | 22 +++++++++--- src/yunohost/tests/test_backuprestore.py | 12 +++---- src/yunohost/tests/test_permission.py | 45 ++++++++++++------------ 3 files changed, 47 insertions(+), 32 deletions(-) diff --git a/data/helpers.d/setting b/data/helpers.d/setting index d083ed563..9deba2dc0 100644 --- a/data/helpers.d/setting +++ b/data/helpers.d/setting @@ -232,11 +232,24 @@ ynh_webpath_register () { # Create a new permission for the app # +# example: ynh_permission_create --permission admin --urls /admin +# # usage: ynh_permission_create --permission "permission" [--urls "url" ["url" ...]] # | arg: permission - the name for the permission (by default a permission named "main" already exist) # | arg: urls - (optional) a list of FULL urls for the permission (e.g. domain.tld/apps/admin) +# | arg: urls - (optional) a list of urls to specify for the permission. +# +# Urls are assumed to be relative to the app domain/path if they start with '/'. +# For example: +# / -> domain.tld/app +# /admin -> domain.tld/app/admin +# domain.tld/app/api -> domain.tld/app/api +# +# Urls can be treated as regexes when they start with "re:". +# For example: +# re:/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ +# re:domain.tld/app/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ # -# example: ynh_permission_create --permission admin --urls domain.tld/blog/admin ynh_permission_create() { declare -Ar args_array=( [p]=permission= [u]=urls= ) local permission @@ -251,10 +264,11 @@ ynh_permission_create() { # Remove a permission for the app (note that when the app is removed all permission is automatically removed) # +# example: ynh_permission_delete --permission editors +# # usage: ynh_permission_delete --permission "permission" # | arg: permission - the name for the permission (by default a permission named "main" is removed automatically when the app is removed) # -# example: ynh_permission_delete --permission editors ynh_permission_delete() { declare -Ar args_array=( [p]=permission= ) local permission @@ -267,8 +281,8 @@ ynh_permission_delete() { # # usage: ynh_permission_urls --permission "permission" --add "url" ["url" ...] --remove "url" ["url" ...] # | arg: permission - the name for the permission (by default a permission named "main" is removed automatically when the app is removed) -# | arg: add - (optional) a list of FULL urls to add to the permission (e.g. domain.tld/apps/admin) -# | arg: remove - (optional) a list of FULL urls to remove from the permission (e.g. other.tld/apps/admin) +# | arg: add - (optional) a list of urls to add to the permission (see permission_create for details regarding their format) +# | arg: remove - (optional) a list of urls to remove from the permission (see permission_create for details regarding their format) # ynh_permission_urls() { declare -Ar args_array=([p]=permission= [a]=add= [r]=remove=) diff --git a/src/yunohost/tests/test_backuprestore.py b/src/yunohost/tests/test_backuprestore.py index bdaf25299..cab98089b 100644 --- a/src/yunohost/tests/test_backuprestore.py +++ b/src/yunohost/tests/test_backuprestore.py @@ -529,9 +529,9 @@ def test_backup_and_restore_permission_app(): assert "permissions_app.main" in res assert "permissions_app.admin" in res assert "permissions_app.dev" in res - assert res['permissions_app.main']['urls'] == [maindomain + "/urlpermissionapp"] - assert res['permissions_app.admin']['urls'] == [maindomain + "/urlpermissionapp/admin"] - assert res['permissions_app.dev']['urls'] == [maindomain + "/urlpermissionapp/dev"] + assert res['permissions_app.main']['urls'] == ["/"] + assert res['permissions_app.admin']['urls'] == ["/admin"] + assert res['permissions_app.dev']['urls'] == ["/dev"] assert res['permissions_app.main']['allowed'] == ["all_users"] assert res['permissions_app.admin']['allowed'] == ["alice"] @@ -543,9 +543,9 @@ def test_backup_and_restore_permission_app(): assert "permissions_app.main" in res assert "permissions_app.admin" in res assert "permissions_app.dev" in res - assert res['permissions_app.main']['urls'] == [maindomain + "/urlpermissionapp"] - assert res['permissions_app.admin']['urls'] == [maindomain + "/urlpermissionapp/admin"] - assert res['permissions_app.dev']['urls'] == [maindomain + "/urlpermissionapp/dev"] + assert res['permissions_app.main']['urls'] == ["/"] + assert res['permissions_app.admin']['urls'] == ["/admin"] + assert res['permissions_app.dev']['urls'] == ["/dev"] assert res['permissions_app.main']['allowed'] == ["all_users"] assert res['permissions_app.admin']['allowed'] == ["alice"] diff --git a/src/yunohost/tests/test_permission.py b/src/yunohost/tests/test_permission.py index bef042be1..a8b6b8258 100644 --- a/src/yunohost/tests/test_permission.py +++ b/src/yunohost/tests/test_permission.py @@ -31,7 +31,7 @@ def setup_function(function): user_create("alice", "Alice", "White", "alice@" + maindomain, dummy_password) user_create("bob", "Bob", "Snow", "bob@" + maindomain, dummy_password) - permission_create("wiki.main", urls=[maindomain + "/wiki"], sync_perm=False) + permission_create("wiki.main", urls=["/"], sync_perm=False) permission_create("blog.main", sync_perm=False) user_permission_update("blog.main", remove="all_users", add="alice") @@ -198,7 +198,7 @@ def test_permission_list(): assert res['blog.main']['allowed'] == ["alice"] assert set(res['wiki.main']['corresponding_users']) == set(["alice", "bob"]) assert res['blog.main']['corresponding_users'] == ["alice"] - assert res['wiki.main']['urls'] == [maindomain + "/wiki"] + assert res['wiki.main']['urls'] == ["/"] # # Create - Remove functions @@ -322,37 +322,37 @@ def test_permission_update_permission_that_doesnt_exist(): # Permission url management def test_permission_add_url(): - permission_urls("blog.main", add=[maindomain + "/testA"]) + permission_urls("blog.main", add=["/testA"]) res = user_permission_list(full=True)['permissions'] - assert res["blog.main"]["urls"] == [maindomain + "/testA"] + assert res["blog.main"]["urls"] == ["/testA"] -def test_permission_add_second_url(): - permission_urls("wiki.main", add=[maindomain + "/testA"]) +def test_permission_add_another_url(): + permission_urls("wiki.main", add=["/testA"]) res = user_permission_list(full=True)['permissions'] - assert set(res["wiki.main"]["urls"]) == set([maindomain + "/testA", maindomain + "/wiki"]) + assert set(res["wiki.main"]["urls"]) == set(["/", "/testA"]) def test_permission_remove_url(): - permission_urls("wiki.main", remove=[maindomain + "/wiki"]) + permission_urls("wiki.main", remove=["/"]) res = user_permission_list(full=True)['permissions'] assert res["wiki.main"]["urls"] == [] def test_permission_add_url_already_added(): res = user_permission_list(full=True)['permissions'] - assert res["wiki.main"]["urls"] == [maindomain + "/wiki"] + assert res["wiki.main"]["urls"] == ["/"] - permission_urls("wiki.main", add=[maindomain + "/wiki"]) + permission_urls("wiki.main", add=["/"]) res = user_permission_list(full=True)['permissions'] - assert res["wiki.main"]["urls"] == [maindomain + "/wiki"] + assert res["wiki.main"]["urls"] == ["/"] def test_permission_remove_url_not_added(): - permission_urls("wiki.main", remove=[maindomain + "/doesnt_exist"]) + permission_urls("wiki.main", remove=["/doesnt_exist"]) res = user_permission_list(full=True)['permissions'] - assert res['wiki.main']['urls'] == [maindomain + "/wiki"] + assert res['wiki.main']['urls'] == ["/"] # # Application interaction @@ -366,9 +366,9 @@ def test_permission_app_install(): assert "permissions_app.main" in res assert "permissions_app.admin" in res assert "permissions_app.dev" in res - assert res['permissions_app.main']['urls'] == [maindomain + "/urlpermissionapp"] - assert res['permissions_app.admin']['urls'] == [maindomain + "/urlpermissionapp/admin"] - assert res['permissions_app.dev']['urls'] == [maindomain + "/urlpermissionapp/dev"] + assert res['permissions_app.main']['urls'] == ["/"] + assert res['permissions_app.admin']['urls'] == ["/admin"] + assert res['permissions_app.dev']['urls'] == ["/dev"] assert res['permissions_app.main']['allowed'] == ["all_users"] assert set(res['permissions_app.main']['corresponding_users']) == set(["alice", "bob"]) @@ -399,17 +399,18 @@ def test_permission_app_change_url(): app_install("./tests/apps/permissions_app_ynh", args="domain=%s&path=%s&admin=%s" % (maindomain, "/urlpermissionapp", "alice"), force=True) + # FIXME : should rework this test to look for differences in the generated app map / app tiles ... res = user_permission_list(full=True)['permissions'] - assert res['permissions_app.main']['urls'] == [maindomain + "/urlpermissionapp"] - assert res['permissions_app.admin']['urls'] == [maindomain + "/urlpermissionapp/admin"] - assert res['permissions_app.dev']['urls'] == [maindomain + "/urlpermissionapp/dev"] + assert res['permissions_app.main']['urls'] == ["/"] + assert res['permissions_app.admin']['urls'] == ["/admin"] + assert res['permissions_app.dev']['urls'] == ["/dev"] app_change_url("permissions_app", maindomain, "/newchangeurl") res = user_permission_list(full=True)['permissions'] - assert res['permissions_app.main']['urls'] == [maindomain + "/newchangeurl"] - assert res['permissions_app.admin']['urls'] == [maindomain + "/newchangeurl/admin"] - assert res['permissions_app.dev']['urls'] == [maindomain + "/newchangeurl/dev"] + assert res['permissions_app.main']['urls'] == ["/"] + assert res['permissions_app.admin']['urls'] == ["/admin"] + assert res['permissions_app.dev']['urls'] == ["/dev"] def test_permission_app_propagation_on_ssowat(): From 7102c5d0ca1ce36a3ff90e3fcb688d4a5be0d221 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 20 Sep 2019 20:14:44 +0200 Subject: [PATCH 13/35] Propagate the new relative url stuff to app_ssowatconf and actuall implement the whole permission system thing in app_map (related to ssowatconf) --- src/yunohost/app.py | 126 ++++++++++++++++++++++---- src/yunohost/tests/test_permission.py | 3 + 2 files changed, 113 insertions(+), 16 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 231568439..51030021f 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -427,25 +427,97 @@ def app_map(app=None, raw=False, user=None): if 'path' not in app_settings: # we assume that an app that doesn't have a path doesn't have an HTTP api continue + # This 'no_sso' settings sound redundant to not having $path defined .... + # At least from what I can see, all apps using it don't have a path defined ... if 'no_sso' in app_settings: # I don't think we need to check for the value here continue + # Users must at least have access to the main permission to have access to extra permissions if user: main_perm = permissions[app_id + ".main"] if user not in main_perm["corresponding_users"] and "visitors" not in main_perm["allowed"]: continue domain = app_settings['domain'] - path = app_settings['path'] + path = app_settings['path'].rstrip('/') + label = app_settings['label'] - if raw: - if domain not in result: - result[domain] = {} - result[domain][path] = { - 'label': app_settings['label'], - 'id': app_settings['id'] - } - else: - result[domain + path] = app_settings['label'] + def _sanitized_absolute_url(perm_url): + # Nominal case : url is relative to the app's path + if perm_url.startswith("/"): + perm_domain = domain + perm_path = path + perm_url.rstrip("/") + # Otherwise, the urls starts with a domain name, like domain.tld/foo/bar + # We want perm_domain = domain.tld and perm_path = "/foo/bar" + else: + perm_domain, perm_path = perm_url.split("/", 1) + perm_path = "/" + perm_path.rstrip("/") + + return perm_domain, perm_path + + this_app_perms = {p: i for p, i in permissions.items() if p.startswith(app_id + ".") and i["urls"]} + for perm_name, perm_info in this_app_perms.items(): + # If we're building the map for a specific user, check the user + # actually is allowed for this specific perm + if user and user not in perm_info["corresponding_users"] and "visitors" not in perm_info["allowed"]: + continue + if len(perm_info["urls"]) > 1 or perm_info["urls"][0].startswith("re:"): + # + # Here we have a big conceptual issue about the sso ... + # Let me take a sip of coffee and turn off the music... + # + # Let's say we have an app foo which created a permission + # 'foo.admin' and added as url "/admin" and "/api" This + # permission got defined somehow as only accessible for group + # "admins". So both "/admin" and "/api" are protected. Good! + # + # Now if we really want users in group "admins" to access those + # uris, then each users in group "admins" need to have these + # urls in the ssowat dict for this user. Which corresponds to a + # tile. To put it otherwise : in the current code of ssowat, a + # permission = a tile = a url ! + # + # We also have an issue if the url define is a regex, because + # the url we want to add to the dict is going to be turned into + # a clickable link (or analyzed by other parts of yunohost + # code...). To put it otherwise : in the current code of ssowat, + # you can't give access a user to a regex + # + # Instead, as drafted by Josue, we could rework the ssowat logic + # about how routes and their permissions are defined. So for example, + # have a dict of + # { "/route1": ["visitors", "user1", "user2", ...], # Public route + # "/route2_with_a_regex$": ["user1", "user2"], # Private route + # "/route3": None, # Skipped route idk + # } + # then each time a user try to request and url, we only keep the + # longest matching rule and check the user is allowed etc... + # + # The challenge with this is (beside actually implementing it) + # is that it creates a whole new mechanism that ultimately + # replace all the existing logic about + # protected/unprotected/skipped uris and regexes and we gotta + # handle / migrate all the legacy stuff somehow if we don't + # want to end up with a total mess in the future idk + logger.error("Permission %s can't be added to the SSOwat configuration because it uses multiple urls and/or uses a regex url" % perm_name) + continue + + perm_domain, perm_path = _sanitized_absolute_url(perm_info["urls"][0]) + + if perm_name.endswith(".main"): + perm_label = label + else: + # e.g. if perm_name is wordpress.admin, we want "Blog (Admin)" (where Blog is the label of this app) + perm_label = "%s (%s)" % (label, perm_name.rsplit(".")[-1].replace("_", " ").title()) + + if raw: + if domain not in result: + result[perm_domain] = {} + result[perm_domain][perm_path] = { + 'label': perm_label, + 'id': app_id + } + else: + result[perm_domain + perm_path] = perm_label return result @@ -1382,13 +1454,34 @@ def app_ssowatconf(): app_settings = read_yaml(APPS_SETTING_PATH + app['id'] + '/settings.yml') + if 'domain' not in app_settings: + continue + if 'path' not in app_settings: + continue + + # This 'no_sso' settings sound redundant to not having $path defined .... + # At least from what I can see, all apps using it don't have a path defined ... if 'no_sso' in app_settings: continue - app_root_webpath = app_settings['domain'] + app_settings['path'].rstrip('/') + domain = app_settings['domain'] + path = app_settings['path'].rstrip('/') + + def _sanitized_absolute_url(perm_url): + # Nominal case : url is relative to the app's path + if perm_url.startswith("/"): + perm_domain = domain + perm_path = path + perm_url.rstrip("/") + # Otherwise, the urls starts with a domain name, like domain.tld/foo/bar + # We want perm_domain = domain.tld and perm_path = "/foo/bar" + else: + perm_domain, perm_path = perm_url.split("/", 1) + perm_path = "/" + perm_path.rstrip("/") + + return perm_domain + perm_path # Skipped - skipped_urls += [app_root_webpath + uri.rstrip("/") for uri in _get_setting(app_settings, 'skipped_uris')] + skipped_urls += [_sanitized_absolute_url(uri) for uri in _get_setting(app_settings, 'skipped_uris')] skipped_regex += _get_setting(app_settings, 'skipped_regex') # Redirected @@ -1396,15 +1489,16 @@ def app_ssowatconf(): redirected_regex.update(app_settings.get('redirected_regex', {})) # Legacy permission system using (un)protected_uris and _regex managed in app settings... - unprotected_urls += [app_root_webpath + uri.rstrip("/") for uri in _get_setting(app_settings, 'unprotected_uris')] - unprotected_urls += [app_root_webpath + uri.rstrip("/") for uri in _get_setting(app_settings, 'protected_uris')] + unprotected_urls += [_sanitized_absolute_url(uri) for uri in _get_setting(app_settings, 'unprotected_uris')] + protected_urls += [_sanitized_absolute_url(uri) for uri in _get_setting(app_settings, 'protected_uris')] unprotected_regex += _get_setting(app_settings, 'unprotected_regex') - unprotected_regex += _get_setting(app_settings, 'protected_regex') + protected_regex += _get_setting(app_settings, 'protected_regex') # New permission system this_app_perms = {name: info for name, info in all_permissions.items() if name.startswith(app['id'] + ".")} for perm_name, perm_info in this_app_perms.items(): - urls = [url.rstrip("/") for url in perm_info["urls"]] + # FIXME : gotta handle regex-urls here... meh + urls = [_sanitized_absolute_url(url) for url in perm_info["urls"]] if "visitors" in perm_info["allowed"]: unprotected_urls += urls diff --git a/src/yunohost/tests/test_permission.py b/src/yunohost/tests/test_permission.py index a8b6b8258..4dc021496 100644 --- a/src/yunohost/tests/test_permission.py +++ b/src/yunohost/tests/test_permission.py @@ -434,6 +434,9 @@ def test_permission_app_propagation_on_ssowat(): # Test admin access, as configured during install, only alice should be able to access it + # alice gotta be allowed on the main permission to access the admin tho + user_permission_update("permissions_app.main", remove="bob", add="all_users") + assert not can_access_webpage(app_webroot+"/admin", logged_as=None) assert can_access_webpage(app_webroot+"/admin", logged_as="alice") assert not can_access_webpage(app_webroot+"/admin", logged_as="bob") From 2a5053b66bfc7bd48cd8f7668db1ee93b1103fe7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 21 Sep 2019 13:32:40 +0200 Subject: [PATCH 14/35] Misc wording and orthotypography... MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Allan Nordhøy --- data/helpers.d/setting | 8 ++++---- locales/en.json | 2 +- src/yunohost/permission.py | 12 ++++++------ src/yunohost/tests/test_permission.py | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/data/helpers.d/setting b/data/helpers.d/setting index 9deba2dc0..b2a647993 100644 --- a/data/helpers.d/setting +++ b/data/helpers.d/setting @@ -177,7 +177,7 @@ else: if key in ['redirected_urls', 'redirected_regex']: value = yaml.load(value) if key in ["unprotected_uris", "unprotected_regex", "protected_uris", "protected_regex"]: - logger.warning("/!\\ Packagers ! This app is using the legacy permission system. Please delete these legacy settings and use the new helpers ynh_permission_{create,urls,update,delete} and the 'visitors' group to manage public/private access.") + logger.warning("/!\\ Packagers! This app is using the legacy permission system. Please delete these legacy settings and use the new helpers 'ynh_permission_{create,urls,update,delete}' and the 'visitors' group to manage public/private access.") settings[key] = value else: raise ValueError("action should either be get, set or delete") @@ -237,15 +237,15 @@ ynh_webpath_register () { # usage: ynh_permission_create --permission "permission" [--urls "url" ["url" ...]] # | arg: permission - the name for the permission (by default a permission named "main" already exist) # | arg: urls - (optional) a list of FULL urls for the permission (e.g. domain.tld/apps/admin) -# | arg: urls - (optional) a list of urls to specify for the permission. +# | arg: urls - (optional) a list of URLs to specify for the permission. # -# Urls are assumed to be relative to the app domain/path if they start with '/'. +# URLs are assumed to be relative to the app domain/path if they start with '/'. # For example: # / -> domain.tld/app # /admin -> domain.tld/app/admin # domain.tld/app/api -> domain.tld/app/api # -# Urls can be treated as regexes when they start with "re:". +# URLs can be treated as regexes when they start with "re:". # For example: # re:/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ # re:domain.tld/app/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ diff --git a/locales/en.json b/locales/en.json index 5df21b684..70b9fa626 100644 --- a/locales/en.json +++ b/locales/en.json @@ -230,7 +230,7 @@ "group_already_exist_on_system": "Group {group} already exists in the system group", "group_created": "Group '{group}' successfully created", "group_creation_failed": "Failed to create group {group}: {error}", - "group_cannot_edit_all_users": "The group 'all_users' cannot be edited manually. It is a special group meant to contain all users registered in Yunohost", + "group_cannot_edit_all_users": "The group 'all_users' cannot be edited manually. It is a special group meant to contain all users registered in YunoHost", "group_cannot_edit_visitors": "The group 'visitors' cannot be edited manually. It is a special group representing anonymous visitors", "group_cannot_edit_primary_group": "The group '{group}' cannot be edited manually. It is the primary group meant to contain only one specific user.", "group_cannot_be_edited": "The group {group} cannot be edited manually.", diff --git a/src/yunohost/permission.py b/src/yunohost/permission.py index 5f9a88e11..75e3f6037 100644 --- a/src/yunohost/permission.py +++ b/src/yunohost/permission.py @@ -146,11 +146,11 @@ def user_permission_update(operation_logger, permission, add=None, remove=None, if "all_users" in new_allowed_groups: # FIXME : i18n # FIXME : write a better explanation ? - logger.warning("This permission is currently enabled for all users in addition to other groups. You probably want to either remove the 'all_users' permission or remove the other groups currently allowed.") + logger.warning("This permission is currently granted to all users in addition to other groups. You probably want to either remove the 'all_users' permission or remove the other groups it is currently granted to.") if "visitors" in new_allowed_groups: # FIXME : i18n # FIXME : write a better explanation ? - logger.warning("This permission is currently enabled for visitors in addition to other groups. You probably want to either remove the 'visitors' permission or remove the other groups currently allowed.") + logger.warning("This permission is currently granted to visitors in addition to other groups. You probably want to either remove the 'visitors' permission or remove the other groups it is currently granted to.") # Don't update LDAP if we update exactly the same values if set(new_allowed_groups) == set(current_allowed_groups): @@ -268,7 +268,7 @@ def permission_create(operation_logger, permission, urls=None, sync_perm=True): Keyword argument: permission -- Name of the permission (e.g. mail or nextcloud or wordpress.editors) - urls -- list of urls to specify for the permission. + urls -- list of URLs to specify for the permission. Urls are assumed to be relative to the app domain/path if they start with '/'. For example: @@ -276,7 +276,7 @@ def permission_create(operation_logger, permission, urls=None, sync_perm=True): /admin -> domain.tld/app/admin domain.tld/app/api -> domain.tld/app/api - Urls can be later treated as regexes when they start with "re:". + URLs can be later treated as regexes when they start with "re:". For example: re:/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ re:domain.tld/app/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ @@ -337,8 +337,8 @@ def permission_urls(operation_logger, permission, add=None, remove=None, sync_pe Keyword argument: permission -- Name of the permission (e.g. mail or nextcloud or wordpress.editors) - add -- List of urls to add (c.f. permission_create for documentation about their format) - remove -- List of urls to remove (c.f. permission_create for documentation about their format) + add -- List of URLs to add (c.f. permission_create for documentation about their format) + remove -- List of URLs to remove (c.f. permission_create for documentation about their format) """ from yunohost.utils.ldap import _get_ldap_interface diff --git a/src/yunohost/tests/test_permission.py b/src/yunohost/tests/test_permission.py index 4dc021496..f17313fa1 100644 --- a/src/yunohost/tests/test_permission.py +++ b/src/yunohost/tests/test_permission.py @@ -180,7 +180,7 @@ def can_access_webpage(webpath, logged_as=None): assert session.cookies r = session.get(webpath, verify=False) - # If we can't access it, we got redirected to the sso + # If we can't access it, we got redirected to the SSO return not r.url.startswith(sso_url) # From 61fb0be7735dcc8369c03de4e066776aefad83e6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 23 Sep 2019 20:57:59 +0200 Subject: [PATCH 15/35] More accurate tests with explicit exception/message excepted to be triggered --- src/yunohost/tests/conftest.py | 23 +++ src/yunohost/tests/test_backuprestore.py | 184 ++++++++++------------- src/yunohost/tests/test_permission.py | 128 ++++++++++------ src/yunohost/tests/test_user-group.py | 146 +++++++++++------- src/yunohost/utils/error.py | 4 +- 5 files changed, 285 insertions(+), 200 deletions(-) diff --git a/src/yunohost/tests/conftest.py b/src/yunohost/tests/conftest.py index a2dc585bd..e23110d1a 100644 --- a/src/yunohost/tests/conftest.py +++ b/src/yunohost/tests/conftest.py @@ -1,9 +1,32 @@ +import pytest import sys import moulinette +from moulinette import m18n +from yunohost.utils.error import YunohostError +from contextlib import contextmanager + sys.path.append("..") + +@contextmanager +def message(mocker, key, **kwargs): + mocker.spy(m18n, "n") + yield + m18n.n.assert_any_call(key, **kwargs) + + +@contextmanager +def raiseYunohostError(mocker, key, **kwargs): + with pytest.raises(YunohostError) as e_info: + yield + assert e_info._excinfo[1].key == key + if kwargs: + assert e_info._excinfo[1].kwargs == kwargs + + + def pytest_addoption(parser): parser.addoption("--yunodebug", action="store_true", default=False) diff --git a/src/yunohost/tests/test_backuprestore.py b/src/yunohost/tests/test_backuprestore.py index cab98089b..ce3e28401 100644 --- a/src/yunohost/tests/test_backuprestore.py +++ b/src/yunohost/tests/test_backuprestore.py @@ -2,14 +2,13 @@ import pytest import os import shutil import subprocess -from mock import ANY -from moulinette import m18n +from conftest import message, raiseYunohostError + from yunohost.app import app_install, app_remove, app_ssowatconf from yunohost.app import _is_installed from yunohost.backup import backup_create, backup_restore, backup_list, backup_info, backup_delete, _recursive_umount from yunohost.domain import _get_maindomain -from yunohost.utils.error import YunohostError from yunohost.user import user_permission_list, user_create, user_list, user_delete from yunohost.tests.test_permission import check_LDAP_db_integrity, check_permission_for_apps @@ -206,10 +205,11 @@ def add_archive_system_from_2p4(): # -def test_backup_only_ldap(): +def test_backup_only_ldap(mocker): # Create the backup - backup_create(system=["conf_ldap"], apps=None) + with message(mocker, "backup_created"): + backup_create(system=["conf_ldap"], apps=None) archives = backup_list()["archives"] assert len(archives) == 1 @@ -222,24 +222,22 @@ def test_backup_only_ldap(): def test_backup_system_part_that_does_not_exists(mocker): - mocker.spy(m18n, "n") - # Create the backup - with pytest.raises(YunohostError): - backup_create(system=["yolol"], apps=None) + with message(mocker, 'backup_hook_unknown', hook="doesnt_exist"): + with raiseYunohostError(mocker, "backup_nothings_done"): + backup_create(system=["doesnt_exist"], apps=None) - m18n.n.assert_any_call('backup_hook_unknown', hook="yolol") - m18n.n.assert_any_call('backup_nothings_done') # # System backup and restore # # -def test_backup_and_restore_all_sys(): +def test_backup_and_restore_all_sys(mocker): # Create the backup - backup_create(system=[], apps=None) + with message(mocker, "backup_created"): + backup_create(system=[], apps=None) archives = backup_list()["archives"] assert len(archives) == 1 @@ -255,8 +253,9 @@ def test_backup_and_restore_all_sys(): assert not os.path.exists("/etc/ssowat/conf.json") # Restore the backup - backup_restore(name=archives[0], force=True, - system=[], apps=None) + with message(mocker, "restore_complete"): + backup_restore(name=archives[0], force=True, + system=[], apps=None) # Check ssowat conf is back assert os.path.exists("/etc/ssowat/conf.json") @@ -270,16 +269,18 @@ def test_backup_and_restore_all_sys(): def test_restore_system_from_Ynh2p4(monkeypatch, mocker): # Backup current system - backup_create(system=[], apps=None) + with message(mocker, "backup_created"): + backup_create(system=[], apps=None) archives = backup_list()["archives"] assert len(archives) == 2 # Restore system archive from 2.4 try: - backup_restore(name=backup_list()["archives"][1], - system=[], - apps=None, - force=True) + with message(mocker, "restore_complete"): + backup_restore(name=backup_list()["archives"][1], + system=[], + apps=None, + force=True) finally: # Restore system as it was backup_restore(name=backup_list()["archives"][0], @@ -306,12 +307,10 @@ def test_backup_script_failure_handling(monkeypatch, mocker): # call with monkeypatch). We also patch m18n to check later it's been called # with the expected error message key monkeypatch.setattr("yunohost.backup.hook_exec", custom_hook_exec) - mocker.spy(m18n, "n") - with pytest.raises(YunohostError): - backup_create(system=None, apps=["backup_recommended_app"]) - - m18n.n.assert_any_call('backup_app_failed', app='backup_recommended_app') + with message(mocker, 'backup_app_failed', app='backup_recommended_app'): + with raiseYunohostError(mocker, 'backup_nothings_done'): + backup_create(system=None, apps=["backup_recommended_app"]) @pytest.mark.with_backup_recommended_app_installed @@ -327,25 +326,17 @@ def test_backup_not_enough_free_space(monkeypatch, mocker): monkeypatch.setattr("yunohost.backup.free_space_in_directory", custom_free_space_in_directory) - mocker.spy(m18n, "n") - - with pytest.raises(YunohostError): + with raiseYunohostError(mocker, 'not_enough_disk_space'): backup_create(system=None, apps=["backup_recommended_app"]) - m18n.n.assert_any_call('not_enough_disk_space', path=ANY) - def test_backup_app_not_installed(mocker): assert not _is_installed("wordpress") - mocker.spy(m18n, "n") - - with pytest.raises(YunohostError): - backup_create(system=None, apps=["wordpress"]) - - m18n.n.assert_any_call("unbackup_app", app="wordpress") - m18n.n.assert_any_call('backup_nothings_done') + with message(mocker, "unbackup_app", app="wordpress"): + with raiseYunohostError(mocker, 'backup_nothings_done'): + backup_create(system=None, apps=["wordpress"]) @pytest.mark.with_backup_recommended_app_installed @@ -355,13 +346,9 @@ def test_backup_app_with_no_backup_script(mocker): os.system("rm %s" % backup_script) assert not os.path.exists(backup_script) - mocker.spy(m18n, "n") - - with pytest.raises(YunohostError): - backup_create(system=None, apps=["backup_recommended_app"]) - - m18n.n.assert_any_call("backup_with_no_backup_script_for_app", app="backup_recommended_app") - m18n.n.assert_any_call('backup_nothings_done') + with message(mocker, "backup_with_no_backup_script_for_app", app="backup_recommended_app"): + with raiseYunohostError(mocker, 'backup_nothings_done'): + backup_create(system=None, apps=["backup_recommended_app"]) @pytest.mark.with_backup_recommended_app_installed @@ -371,23 +358,21 @@ def test_backup_app_with_no_restore_script(mocker): os.system("rm %s" % restore_script) assert not os.path.exists(restore_script) - mocker.spy(m18n, "n") - # Backuping an app with no restore script will only display a warning to the # user... - backup_create(system=None, apps=["backup_recommended_app"]) - - m18n.n.assert_any_call("backup_with_no_restore_script_for_app", app="backup_recommended_app") + with message(mocker, "backup_with_no_restore_script_for_app", app="backup_recommended_app"): + backup_create(system=None, apps=["backup_recommended_app"]) @pytest.mark.clean_opt_dir -def test_backup_with_different_output_directory(): +def test_backup_with_different_output_directory(mocker): # Create the backup - backup_create(system=["conf_ssh"], apps=None, - output_directory="/opt/test_backup_output_directory", - name="backup") + with message(mocker, "backup_created"): + backup_create(system=["conf_ssh"], apps=None, + output_directory="/opt/test_backup_output_directory", + name="backup") assert os.path.exists("/opt/test_backup_output_directory/backup.tar.gz") @@ -401,12 +386,14 @@ def test_backup_with_different_output_directory(): @pytest.mark.clean_opt_dir -def test_backup_with_no_compress(): +def test_backup_with_no_compress(mocker): + # Create the backup - backup_create(system=["conf_nginx"], apps=None, - output_directory="/opt/test_backup_output_directory", - no_compress=True, - name="backup") + with message(mocker, "backup_created"): + backup_create(system=["conf_nginx"], apps=None, + output_directory="/opt/test_backup_output_directory", + no_compress=True, + name="backup") assert os.path.exists("/opt/test_backup_output_directory/info.json") @@ -416,10 +403,11 @@ def test_backup_with_no_compress(): # @pytest.mark.with_wordpress_archive_from_2p4 -def test_restore_app_wordpress_from_Ynh2p4(): +def test_restore_app_wordpress_from_Ynh2p4(mocker): - backup_restore(system=None, name=backup_list()["archives"][0], - apps=["wordpress"]) + with message(mocker, "restore_complete"): + backup_restore(system=None, name=backup_list()["archives"][0], + apps=["wordpress"]) @pytest.mark.with_wordpress_archive_from_2p4 @@ -431,16 +419,14 @@ def test_restore_app_script_failure_handling(monkeypatch, mocker): raise Exception monkeypatch.setattr("yunohost.backup.hook_exec", custom_hook_exec) - mocker.spy(m18n, "n") assert not _is_installed("wordpress") - with pytest.raises(YunohostError): - backup_restore(system=None, name=backup_list()["archives"][0], - apps=["wordpress"]) + with message(mocker, 'restore_app_failed', app='wordpress'): + with raiseYunohostError(mocker, 'restore_nothings_done'): + backup_restore(system=None, name=backup_list()["archives"][0], + apps=["wordpress"]) - m18n.n.assert_any_call('restore_app_failed', app='wordpress') - m18n.n.assert_any_call('restore_nothings_done') assert not _is_installed("wordpress") @@ -452,18 +438,13 @@ def test_restore_app_not_enough_free_space(monkeypatch, mocker): monkeypatch.setattr("yunohost.backup.free_space_in_directory", custom_free_space_in_directory) - mocker.spy(m18n, "n") assert not _is_installed("wordpress") - with pytest.raises(YunohostError): + with raiseYunohostError(mocker, 'restore_not_enough_disk_space'): backup_restore(system=None, name=backup_list()["archives"][0], apps=["wordpress"]) - m18n.n.assert_any_call('restore_not_enough_disk_space', - free_space=0, - margin=ANY, - needed_space=ANY) assert not _is_installed("wordpress") @@ -473,13 +454,11 @@ def test_restore_app_not_in_backup(mocker): assert not _is_installed("wordpress") assert not _is_installed("yoloswag") - mocker.spy(m18n, "n") + with message(mocker, 'backup_archive_app_not_found', app="yoloswag"): + with raiseYunohostError(mocker, 'restore_nothings_done'): + backup_restore(system=None, name=backup_list()["archives"][0], + apps=["yoloswag"]) - with pytest.raises(YunohostError): - backup_restore(system=None, name=backup_list()["archives"][0], - apps=["yoloswag"]) - - m18n.n.assert_any_call('backup_archive_app_not_found', app="yoloswag") assert not _is_installed("wordpress") assert not _is_installed("yoloswag") @@ -489,38 +468,36 @@ def test_restore_app_already_installed(mocker): assert not _is_installed("wordpress") - backup_restore(system=None, name=backup_list()["archives"][0], - apps=["wordpress"]) - - assert _is_installed("wordpress") - - mocker.spy(m18n, "n") - with pytest.raises(YunohostError): + with message(mocker, "restore_complete"): backup_restore(system=None, name=backup_list()["archives"][0], apps=["wordpress"]) - m18n.n.assert_any_call('restore_already_installed_app', app="wordpress") - m18n.n.assert_any_call('restore_nothings_done') + assert _is_installed("wordpress") + + with message(mocker, 'restore_already_installed_app', app="wordpress"): + with raiseYunohostError(mocker, 'restore_nothings_done'): + backup_restore(system=None, name=backup_list()["archives"][0], + apps=["wordpress"]) assert _is_installed("wordpress") @pytest.mark.with_legacy_app_installed -def test_backup_and_restore_legacy_app(): +def test_backup_and_restore_legacy_app(mocker): - _test_backup_and_restore_app("legacy_app") + _test_backup_and_restore_app(mocker, "legacy_app") @pytest.mark.with_backup_recommended_app_installed -def test_backup_and_restore_recommended_app(): +def test_backup_and_restore_recommended_app(mocker): - _test_backup_and_restore_app("backup_recommended_app") + _test_backup_and_restore_app(mocker, "backup_recommended_app") @pytest.mark.with_backup_recommended_app_installed_with_ynh_restore -def test_backup_and_restore_with_ynh_restore(): +def test_backup_and_restore_with_ynh_restore(mocker): - _test_backup_and_restore_app("backup_recommended_app") + _test_backup_and_restore_app(mocker, "backup_recommended_app") @pytest.mark.with_permission_app_installed def test_backup_and_restore_permission_app(): @@ -552,10 +529,11 @@ def test_backup_and_restore_permission_app(): assert res['permissions_app.dev']['allowed'] == [] -def _test_backup_and_restore_app(app): +def _test_backup_and_restore_app(mocker, app): # Create a backup of this app - backup_create(system=None, apps=[app]) + with message(mocker, "backup_created"): + backup_create(system=None, apps=[app]) archives = backup_list()["archives"] assert len(archives) == 1 @@ -571,8 +549,9 @@ def _test_backup_and_restore_app(app): assert app+".main" not in user_permission_list()['permissions'] # Restore the app - backup_restore(system=None, name=archives[0], - apps=[app]) + with message(mocker, "restore_complete"): + backup_restore(system=None, name=archives[0], + apps=[app]) assert app_is_installed(app) @@ -593,13 +572,11 @@ def test_restore_archive_with_no_json(mocker): assert "badbackup" in backup_list()["archives"] - mocker.spy(m18n, "n") - with pytest.raises(YunohostError): + with raiseYunohostError(mocker, 'backup_invalid_archive'): backup_restore(name="badbackup", force=True) - m18n.n.assert_any_call('backup_invalid_archive') -def test_backup_binds_are_readonly(monkeypatch): +def test_backup_binds_are_readonly(mocker, monkeypatch): def custom_mount_and_backup(self, backup_manager): self.manager = backup_manager @@ -620,4 +597,5 @@ def test_backup_binds_are_readonly(monkeypatch): custom_mount_and_backup) # Create the backup - backup_create(system=[]) + with message(mocker, "backup_created"): + backup_create(system=[]) diff --git a/src/yunohost/tests/test_permission.py b/src/yunohost/tests/test_permission.py index f17313fa1..0f3fb63e0 100644 --- a/src/yunohost/tests/test_permission.py +++ b/src/yunohost/tests/test_permission.py @@ -1,19 +1,20 @@ import requests import pytest -from yunohost.app import app_install, app_remove, app_change_url, app_list, app_map +from conftest import message, raiseYunohostError -from yunohost.user import user_list, user_info, user_create, user_delete, user_update, \ - user_group_list, user_group_create, user_group_delete, user_group_update, user_group_info +from yunohost.app import app_install, app_remove, app_change_url, app_list, app_map +from yunohost.user import user_list, user_create, user_delete, \ + user_group_list, user_group_delete from yunohost.permission import user_permission_update, user_permission_list, user_permission_reset, \ permission_create, permission_urls, permission_delete from yunohost.domain import _get_maindomain -from yunohost.utils.error import YunohostError # Get main domain maindomain = _get_maindomain() dummy_password = "test123Ynh" + def clean_user_groups_permission(): for u in user_list()['users']: user_delete(u) @@ -26,6 +27,7 @@ def clean_user_groups_permission(): if any(p.startswith(name) for name in ["wiki", "blog", "site", "permissions_app"]): permission_delete(p, force=True, sync_perm=False) + def setup_function(function): clean_user_groups_permission() @@ -35,6 +37,7 @@ def setup_function(function): permission_create("blog.main", sync_perm=False) user_permission_update("blog.main", remove="all_users", add="alice") + def teardown_function(function): clean_user_groups_permission() try: @@ -42,12 +45,14 @@ def teardown_function(function): except: pass + @pytest.fixture(autouse=True) def check_LDAP_db_integrity_call(): check_LDAP_db_integrity() yield check_LDAP_db_integrity() + def check_LDAP_db_integrity(): # Here we check that all attributes in all object are sychronized. # Here is the list of attributes per object: @@ -162,7 +167,7 @@ def check_permission_for_apps(): def can_access_webpage(webpath, logged_as=None): webpath = webpath.rstrip("/") - sso_url = "https://"+maindomain+"/yunohost/sso/" + sso_url = "https://" + maindomain + "/yunohost/sso/" # Anonymous access if not logged_as: @@ -183,6 +188,7 @@ def can_access_webpage(webpath, logged_as=None): # If we can't access it, we got redirected to the SSO return not r.url.startswith(sso_url) + # # List functions # @@ -204,8 +210,10 @@ def test_permission_list(): # Create - Remove functions # -def test_permission_create_main(): - permission_create("site.main") + +def test_permission_create_main(mocker): + with message(mocker, "permission_created", permission="site.main"): + permission_create("site.main") res = user_permission_list(full=True)['permissions'] assert "site.main" in res @@ -213,8 +221,9 @@ def test_permission_create_main(): assert set(res['site.main']['corresponding_users']) == set(["alice", "bob"]) -def test_permission_create_extra(): - permission_create("site.test") +def test_permission_create_extra(mocker): + with message(mocker, "permission_created", permission="site.test"): + permission_create("site.test") res = user_permission_list(full=True)['permissions'] assert "site.test" in res @@ -222,8 +231,10 @@ def test_permission_create_extra(): assert "all_users" not in res['site.test']['allowed'] assert res['site.test']['corresponding_users'] == [] -def test_permission_delete(): - permission_delete("wiki.main", force=True) + +def test_permission_delete(mocker): + with message(mocker, "permission_deleted", permission="wiki.main"): + permission_delete("wiki.main", force=True) res = user_permission_list()['permissions'] assert "wiki.main" not in res @@ -232,12 +243,14 @@ def test_permission_delete(): # Error on create - remove function # -def test_permission_create_already_existing(): - with pytest.raises(YunohostError): + +def test_permission_create_already_existing(mocker): + with raiseYunohostError(mocker, "permission_already_exist"): permission_create("wiki.main") -def test_permission_delete_doesnt_existing(): - with pytest.raises(YunohostError): + +def test_permission_delete_doesnt_existing(mocker): + with raiseYunohostError(mocker, "permission_not_found"): permission_delete("doesnt.exist", force=True) res = user_permission_list()['permissions'] @@ -246,8 +259,9 @@ def test_permission_delete_doesnt_existing(): assert "mail.main" in res assert "xmpp.main" in res -def test_permission_delete_main_without_force(): - with pytest.raises(YunohostError): + +def test_permission_delete_main_without_force(mocker): + with raiseYunohostError(mocker, "permission_cannot_remove_main"): permission_delete("blog.main") res = user_permission_list()['permissions'] @@ -259,44 +273,55 @@ def test_permission_delete_main_without_force(): # user side functions -def test_permission_add_group(): - user_permission_update("wiki.main", add="alice") + +def test_permission_add_group(mocker): + with message(mocker, "permission_updated", permission="wiki.main"): + user_permission_update("wiki.main", add="alice") res = user_permission_list(full=True)['permissions'] assert set(res['wiki.main']['allowed']) == set(["all_users", "alice"]) assert set(res['wiki.main']['corresponding_users']) == set(["alice", "bob"]) -def test_permission_remove_group(): - user_permission_update("blog.main", remove="alice") + +def test_permission_remove_group(mocker): + with message(mocker, "permission_updated", permission="blog.main"): + user_permission_update("blog.main", remove="alice") res = user_permission_list(full=True)['permissions'] assert res['blog.main']['allowed'] == [] assert res['blog.main']['corresponding_users'] == [] -def test_permission_add_and_remove_group(): - user_permission_update("wiki.main", add="alice", remove="all_users") + +def test_permission_add_and_remove_group(mocker): + with message(mocker, "permission_updated", permission="wiki.main"): + user_permission_update("wiki.main", add="alice", remove="all_users") res = user_permission_list(full=True)['permissions'] assert res['wiki.main']['allowed'] == ["alice"] assert res['wiki.main']['corresponding_users'] == ["alice"] -def test_permission_add_group_already_allowed(): - user_permission_update("blog.main", add="alice") + +def test_permission_add_group_already_allowed(mocker): + with message(mocker, "permission_already_allowed", permission="blog.main", group="alice"): + user_permission_update("blog.main", add="alice") res = user_permission_list(full=True)['permissions'] assert res['blog.main']['allowed'] == ["alice"] assert res['blog.main']['corresponding_users'] == ["alice"] -def test_permission_remove_group_already_not_allowed(): - user_permission_update("blog.main", remove="bob") + +def test_permission_remove_group_already_not_allowed(mocker): + with message(mocker, "permission_already_disallowed", permission="blog.main", group="bob"): + user_permission_update("blog.main", remove="bob") res = user_permission_list(full=True)['permissions'] assert res['blog.main']['allowed'] == ["alice"] assert res['blog.main']['corresponding_users'] == ["alice"] -def test_permission_reset(): - # Reset permission - user_permission_reset("blog.main") + +def test_permission_reset(mocker): + with message(mocker, "permission_updated", permission="blog.main"): + user_permission_reset("blog.main") res = user_permission_list(full=True)['permissions'] assert res['blog.main']['allowed'] == ["all_users"] @@ -306,50 +331,62 @@ def test_permission_reset(): # Error on update function # -def test_permission_add_group_that_doesnt_exist(): - with pytest.raises(YunohostError): + +def test_permission_add_group_that_doesnt_exist(mocker): + with raiseYunohostError(mocker, "group_unknown"): user_permission_update("blog.main", add="doesnt_exist") res = user_permission_list(full=True)['permissions'] assert res['blog.main']['allowed'] == ["alice"] assert res['blog.main']['corresponding_users'] == ["alice"] -def test_permission_update_permission_that_doesnt_exist(): - with pytest.raises(YunohostError): + +def test_permission_update_permission_that_doesnt_exist(mocker): + with raiseYunohostError(mocker, "permission_not_found"): user_permission_update("doesnt.exist", add="alice") # Permission url management -def test_permission_add_url(): - permission_urls("blog.main", add=["/testA"]) + +def test_permission_add_url(mocker): + with message(mocker, "permission_updated", permission="blog.main"): + permission_urls("blog.main", add=["/testA"]) res = user_permission_list(full=True)['permissions'] assert res["blog.main"]["urls"] == ["/testA"] -def test_permission_add_another_url(): - permission_urls("wiki.main", add=["/testA"]) + +def test_permission_add_another_url(mocker): + with message(mocker, "permission_updated", permission="wiki.main"): + permission_urls("wiki.main", add=["/testA"]) res = user_permission_list(full=True)['permissions'] assert set(res["wiki.main"]["urls"]) == set(["/", "/testA"]) -def test_permission_remove_url(): - permission_urls("wiki.main", remove=["/"]) + +def test_permission_remove_url(mocker): + with message(mocker, "permission_updated", permission="wiki.main"): + permission_urls("wiki.main", remove=["/"]) res = user_permission_list(full=True)['permissions'] assert res["wiki.main"]["urls"] == [] -def test_permission_add_url_already_added(): + +def test_permission_add_url_already_added(mocker): res = user_permission_list(full=True)['permissions'] assert res["wiki.main"]["urls"] == ["/"] - permission_urls("wiki.main", add=["/"]) + with message(mocker, "permission_update_nothing_to_do"): + permission_urls("wiki.main", add=["/"]) res = user_permission_list(full=True)['permissions'] assert res["wiki.main"]["urls"] == ["/"] -def test_permission_remove_url_not_added(): - permission_urls("wiki.main", remove=["/doesnt_exist"]) + +def test_permission_remove_url_not_added(mocker): + with message(mocker, "permission_update_nothing_to_do"): + permission_urls("wiki.main", remove=["/doesnt_exist"]) res = user_permission_list(full=True)['permissions'] assert res['wiki.main']['urls'] == ["/"] @@ -358,6 +395,7 @@ def test_permission_remove_url_not_added(): # Application interaction # + def test_permission_app_install(): app_install("./tests/apps/permissions_app_ynh", args="domain=%s&path=%s&is_public=0&admin=%s" % (maindomain, "/urlpermissionapp", "alice"), force=True) @@ -395,6 +433,7 @@ def test_permission_app_remove(): res = user_permission_list(full=True)['permissions'] assert not any(p.startswith("permissions_app.") for p in res.keys()) + def test_permission_app_change_url(): app_install("./tests/apps/permissions_app_ynh", args="domain=%s&path=%s&admin=%s" % (maindomain, "/urlpermissionapp", "alice"), force=True) @@ -441,6 +480,7 @@ def test_permission_app_propagation_on_ssowat(): assert can_access_webpage(app_webroot+"/admin", logged_as="alice") assert not can_access_webpage(app_webroot+"/admin", logged_as="bob") + def test_permission_legacy_app_propagation_on_ssowat(): # TODO / FIXME : To be actually implemented later .... diff --git a/src/yunohost/tests/test_user-group.py b/src/yunohost/tests/test_user-group.py index 30bdeb017..695f09477 100644 --- a/src/yunohost/tests/test_user-group.py +++ b/src/yunohost/tests/test_user-group.py @@ -1,22 +1,25 @@ import pytest +from conftest import message, raiseYunohostError + from yunohost.user import user_list, user_info, user_create, user_delete, user_update, \ - user_group_list, user_group_create, user_group_delete, user_group_update, user_group_info + user_group_list, user_group_create, user_group_delete, user_group_update from yunohost.domain import _get_maindomain -from yunohost.utils.error import YunohostError from yunohost.tests.test_permission import check_LDAP_db_integrity # Get main domain maindomain = _get_maindomain() + def clean_user_groups(): for u in user_list()['users']: user_delete(u) for g in user_group_list()['groups']: - if g != "all_users": + if g not in ["all_users", "visitors"]: user_group_delete(g) + def setup_function(function): clean_user_groups() @@ -29,9 +32,11 @@ def setup_function(function): user_group_update("dev", add=["alice"]) user_group_update("apps", add=["bob"]) + def teardown_function(function): clean_user_groups() + @pytest.fixture(autouse=True) def check_LDAP_db_integrity_call(): check_LDAP_db_integrity() @@ -42,6 +47,7 @@ def check_LDAP_db_integrity_call(): # List functions # + def test_list_users(): res = user_list()['users'] @@ -49,6 +55,7 @@ def test_list_users(): assert "bob" in res assert "jack" in res + def test_list_groups(): res = user_group_list()['groups'] @@ -65,8 +72,11 @@ def test_list_groups(): # Create - Remove functions # -def test_create_user(): - user_create("albert", "Albert", "Good", "alber@" + maindomain, "test123Ynh") + +def test_create_user(mocker): + + with message(mocker, "user_created"): + user_create("albert", "Albert", "Good", "alber@" + maindomain, "test123Ynh") group_res = user_group_list()['groups'] assert "albert" in user_list()['users'] @@ -74,24 +84,33 @@ def test_create_user(): assert "albert" in group_res['albert']['members'] assert "albert" in group_res['all_users']['members'] -def test_del_user(): - user_delete("alice") + +def test_del_user(mocker): + + with message(mocker, "user_deleted"): + user_delete("alice") group_res = user_group_list()['groups'] assert "alice" not in user_list() assert "alice" not in group_res assert "alice" not in group_res['all_users']['members'] -def test_create_group(): - user_group_create("adminsys") + +def test_create_group(mocker): + + with message(mocker, "group_created", group="adminsys"): + user_group_create("adminsys") group_res = user_group_list()['groups'] assert "adminsys" in group_res assert "members" in group_res['adminsys'].keys() assert group_res["adminsys"]["members"] == [] -def test_del_group(): - user_group_delete("dev") + +def test_del_group(mocker): + + with message(mocker, "group_deleted", group="dev"): + user_group_delete("dev") group_res = user_group_list()['groups'] assert "dev" not in group_res @@ -100,75 +119,94 @@ def test_del_group(): # Error on create / remove function # -def test_create_user_with_mail_address_already_taken(): - with pytest.raises(YunohostError): + +def test_create_user_with_mail_address_already_taken(mocker): + with raiseYunohostError(mocker, "user_creation_failed"): user_create("alice2", "Alice", "White", "alice@" + maindomain, "test123Ynh") -def test_create_user_with_password_too_simple(): - with pytest.raises(YunohostError): + +def test_create_user_with_password_too_simple(mocker): + with raiseYunohostError(mocker, "password_listed"): user_create("other", "Alice", "White", "other@" + maindomain, "12") -def test_create_user_already_exists(): - with pytest.raises(YunohostError): + +def test_create_user_already_exists(mocker): + with raiseYunohostError(mocker, "user_already_exists"): user_create("alice", "Alice", "White", "other@" + maindomain, "test123Ynh") -def test_update_user_with_mail_address_already_taken(): - with pytest.raises(YunohostError): - user_update("bob", add_mailalias="alice@" + maindomain) -def test_del_user_that_does_not_exist(): - with pytest.raises(YunohostError): +def test_update_user_with_mail_address_already_taken(mocker): + with raiseYunohostError(mocker, "user_update_failed"): + user_update("bob", add_mailalias="alice@" + maindomain) + + +def test_del_user_that_does_not_exist(mocker): + with raiseYunohostError(mocker, "user_unknown"): user_delete("doesnt_exist") -def test_create_group_all_users(): + +def test_create_group_all_users(mocker): # Check groups already exist with special group "all_users" - with pytest.raises(YunohostError): + with raiseYunohostError(mocker, "group_already_exist"): user_group_create("all_users") -def test_create_group_already_exists(): + +def test_create_group_already_exists(mocker): # Check groups already exist (regular groups) - with pytest.raises(YunohostError): + with raiseYunohostError(mocker, "group_already_exist"): user_group_create("dev") -def test_del_group_all_users(): - with pytest.raises(YunohostError): + +def test_del_group_all_users(mocker): + with raiseYunohostError(mocker, "group_cannot_be_deleted"): user_group_delete("all_users") -def test_del_group_that_does_not_exist(): - with pytest.raises(YunohostError): + +def test_del_group_that_does_not_exist(mocker): + with raiseYunohostError(mocker, "group_unknown"): user_group_delete("doesnt_exist") # # Update function # -def test_update_user(): - user_update("alice", firstname="NewName", lastname="NewLast") + +def test_update_user(mocker): + with message(mocker, "user_updated"): + user_update("alice", firstname="NewName", lastname="NewLast") info = user_info("alice") assert info['firstname'] == "NewName" assert info['lastname'] == "NewLast" -def test_update_group_add_user(): - user_group_update("dev", add=["bob"]) + +def test_update_group_add_user(mocker): + with message(mocker, "group_updated", group="dev"): + user_group_update("dev", add=["bob"]) group_res = user_group_list()['groups'] assert set(group_res['dev']['members']) == set(["alice", "bob"]) -def test_update_group_add_user_already_in(): - user_group_update("apps", add=["bob"]) + +def test_update_group_add_user_already_in(mocker): + with message(mocker, "group_user_already_in_group", user="bob", group="apps"): + user_group_update("apps", add=["bob"]) group_res = user_group_list()['groups'] assert group_res['apps']['members'] == ["bob"] -def test_update_group_remove_user(): - user_group_update("apps", remove=["bob"]) + +def test_update_group_remove_user(mocker): + with message(mocker, "group_updated", group="apps"): + user_group_update("apps", remove=["bob"]) group_res = user_group_list()['groups'] assert group_res['apps']['members'] == [] -def test_update_group_remove_user_not_already_in(): - user_group_update("apps", remove=["jack"]) + +def test_update_group_remove_user_not_already_in(mocker): + with message(mocker, "group_user_not_in_group", user="jack", group="apps"): + user_group_update("apps", remove=["jack"]) group_res = user_group_list()['groups'] assert group_res['apps']['members'] == ["bob"] @@ -177,29 +215,33 @@ def test_update_group_remove_user_not_already_in(): # Error on update functions # -def test_update_user_that_doesnt_exist(): - with pytest.raises(YunohostError): + +def test_update_user_that_doesnt_exist(mocker): + with raiseYunohostError(mocker, "user_unknown"): user_update("doesnt_exist", firstname="NewName", lastname="NewLast") -def test_update_group_that_doesnt_exist(): - # Check groups not found - with pytest.raises(YunohostError): + +def test_update_group_that_doesnt_exist(mocker): + with raiseYunohostError(mocker, "group_unknown"): user_group_update("doesnt_exist", add=["alice"]) -def test_update_group_all_users_manually(): - with pytest.raises(YunohostError): + +def test_update_group_all_users_manually(mocker): + with raiseYunohostError(mocker, "group_cannot_edit_all_users"): user_group_update("all_users", remove=["alice"]) assert "alice" in user_group_list()["groups"]["all_users"]["members"] -def test_update_group_primary_manually(): - with pytest.raises(YunohostError): + +def test_update_group_primary_manually(mocker): + with raiseYunohostError(mocker, "group_cannot_edit_primary_group"): user_group_update("alice", remove=["alice"]) + assert "alice" in user_group_list()["groups"]["alice"]["members"] -def test_update_group_add_user_that_doesnt_exist(): - # Check add bad user in group - with pytest.raises(YunohostError): + +def test_update_group_add_user_that_doesnt_exist(mocker): + with raiseYunohostError(mocker, "user_unknown"): user_group_update("dev", add=["doesnt_exist"]) assert "doesnt_exist" not in user_group_list()["groups"]["dev"]["members"] diff --git a/src/yunohost/utils/error.py b/src/yunohost/utils/error.py index aeffabcf0..75cf35093 100644 --- a/src/yunohost/utils/error.py +++ b/src/yunohost/utils/error.py @@ -27,12 +27,14 @@ class YunohostError(MoulinetteError): """ Yunohost base exception - + The (only?) main difference with MoulinetteError being that keys are translated via m18n.n (namespace) instead of m18n.g (global?) """ def __init__(self, key, raw_msg=False, *args, **kwargs): + self.key = key # Saving the key is useful for unit testing + self.kwargs = kwargs # Saving the key is useful for unit testing if raw_msg: msg = key else: From 35bfe97d50b1a6bb44f16ccddaf376723e304c57 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 25 Sep 2019 22:08:47 +0200 Subject: [PATCH 16/35] Copy pasta typo : all_users -> visitors Co-Authored-By: Josue-T --- src/yunohost/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 581354f77..312b131b9 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -678,7 +678,7 @@ def user_group_update(operation_logger, groupname, add=None, remove=None, force= if not force: if groupname == "all_users": raise YunohostError('group_cannot_edit_all_users') - elif groupname == "all_users": + elif groupname == "visitors": raise YunohostError('group_cannot_edit_visitors') elif groupname in existing_users: raise YunohostError('group_cannot_edit_primary_group', group=groupname) From 4a14cbd6e0bc092c7bf3ed94f994d9330dc1c1a0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 9 Oct 2019 18:42:17 +0200 Subject: [PATCH 17/35] Fix / implement remaining test --- src/yunohost/tests/test_permission.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/yunohost/tests/test_permission.py b/src/yunohost/tests/test_permission.py index f17313fa1..a9e16cfc6 100644 --- a/src/yunohost/tests/test_permission.py +++ b/src/yunohost/tests/test_permission.py @@ -41,6 +41,10 @@ def teardown_function(function): app_remove("permissions_app") except: pass + try: + app_remove("legacy_app") + except: + pass @pytest.fixture(autouse=True) def check_LDAP_db_integrity_call(): @@ -443,23 +447,22 @@ def test_permission_app_propagation_on_ssowat(): def test_permission_legacy_app_propagation_on_ssowat(): - # TODO / FIXME : To be actually implemented later .... - raise NotImplementedError - app_install("./tests/apps/legacy_app_ynh", args="domain=%s&path=%s" % (maindomain, "/legacy"), force=True) # App is configured as public by default using the legacy unprotected_uri mechanics # It should automatically be migrated during the install - assert res['permissions_app.main']['allowed'] == ["visitors"] + res = user_permission_list(full=True)['permissions'] + assert res['legacy_app.main']['allowed'] == ["visitors"] - assert can_access_webpage(maindomain + "/legacy", logged_as=None) - assert can_access_webpage(maindomain + "/legacy", logged_as="alice") + app_webroot = "https://%s/legacy" % maindomain + + assert can_access_webpage(app_webroot, logged_as=None) + assert can_access_webpage(app_webroot, logged_as="alice") # Try to update the permission and check that permissions are still consistent user_permission_update("legacy_app.main", remove="visitors", add="bob") - res = user_permission_list(full=True)['permissions'] - assert not can_access_webpage(maindomain + "/legacy", logged_as=None) - assert not can_access_webpage(maindomain + "/legacy", logged_as="alice") - assert can_access_webpage(maindomain + "/legacy", logged_as="bob") + assert not can_access_webpage(app_webroot, logged_as=None) + assert not can_access_webpage(app_webroot, logged_as="alice") + assert can_access_webpage(app_webroot, logged_as="bob") From df49af0ad001b99086083798a2b6e888c9352a80 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 9 Oct 2019 18:55:11 +0200 Subject: [PATCH 18/35] Redundant operation considering we're deleting all groups right after --- src/yunohost/data_migrations/0011_setup_group_permission.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/yunohost/data_migrations/0011_setup_group_permission.py b/src/yunohost/data_migrations/0011_setup_group_permission.py index dd5b3c274..ae5a8bfb9 100644 --- a/src/yunohost/data_migrations/0011_setup_group_permission.py +++ b/src/yunohost/data_migrations/0011_setup_group_permission.py @@ -60,7 +60,6 @@ class MyMigration(Migration): ldap_map = read_yaml('/usr/share/yunohost/yunohost-config/moulinette/ldap_scheme.yml') try: - self.remove_if_exists("cn=sftpusers,ou=groups") self.remove_if_exists("ou=permission") self.remove_if_exists('ou=groups') From 96bc95656c6ad29fa59ae337a0f9f4f04c097261 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 9 Oct 2019 19:22:31 +0200 Subject: [PATCH 19/35] Allow the migration to proceed if slapd config was manually modified, warn the user about where the conf will be backuped --- locales/en.json | 2 +- src/yunohost/data_migrations/0011_setup_group_permission.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/locales/en.json b/locales/en.json index 5c3595782..3e1054069 100644 --- a/locales/en.json +++ b/locales/en.json @@ -346,7 +346,7 @@ "migration_0011_can_not_backup_before_migration": "The backup of the system before the migration failed. Migration failed. Error: {error:s}", "migration_0011_create_group": "Creating a group for each user…", "migration_0011_done": "Migration successful. You are now able to manage usergroups.", - "migration_0011_LDAP_config_dirty": "It look like that you customized your LDAP configuration. For this migration the LDAP configuration needs to be updated.\nYou need to save your current configuration, reintialize the original configuration by running 'yunohost tools regen-conf -f' and retry the migration", + "migration_0011_slapd_config_will_be_overwritten": "It looks like you manually edited the slapd configuration. For this critical migration, YunoHost needs to force the update of the slapd configuration. The original files will be backuped in {conf_backup_folder}.", "migration_0011_LDAP_update_failed": "Could not update LDAP. Error: {error:s}", "migration_0011_migrate_permission": "Migrating permissions from apps settings to LDAP…", "migration_0011_migration_failed_trying_to_rollback": "Migration failed… trying to roll back the system.", diff --git a/src/yunohost/data_migrations/0011_setup_group_permission.py b/src/yunohost/data_migrations/0011_setup_group_permission.py index ae5a8bfb9..de28a3ad7 100644 --- a/src/yunohost/data_migrations/0011_setup_group_permission.py +++ b/src/yunohost/data_migrations/0011_setup_group_permission.py @@ -9,7 +9,7 @@ from moulinette.utils.filesystem import read_yaml from yunohost.tools import Migration from yunohost.user import user_group_create, user_group_update from yunohost.app import app_setting, app_list -from yunohost.regenconf import regen_conf +from yunohost.regenconf import regen_conf, BACKUP_CONF_DIR from yunohost.permission import permission_create, user_permission_update, permission_sync_to_user logger = getActionLogger('yunohost.migration') @@ -130,7 +130,7 @@ class MyMigration(Migration): ldap_regen_conf_status = regen_conf(names=['slapd'], dry_run=True) # By this we check if the have been customized if ldap_regen_conf_status and ldap_regen_conf_status['slapd']['pending']: - raise YunohostError("migration_0011_LDAP_config_dirty") + logger.warning("migration_0011_slapd_config_will_be_overwritten", conf_backup_folder=BACKUP_CONF_DIR) # Backup LDAP and the apps settings before to do the migration logger.info(m18n.n("migration_0011_backup_before_migration")) From 9cecd71437d050696a5e98e676532c21e8396749 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 9 Oct 2019 19:39:37 +0200 Subject: [PATCH 20/35] Fix permission_reset idempotency --- src/yunohost/permission.py | 4 ++++ src/yunohost/tests/test_permission.py | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/yunohost/permission.py b/src/yunohost/permission.py index 75e3f6037..97e5b4122 100644 --- a/src/yunohost/permission.py +++ b/src/yunohost/permission.py @@ -217,6 +217,10 @@ def user_permission_reset(operation_logger, permission, sync_perm=True): if existing_permission is None: raise YunohostError('permission_not_found', permission=permission) + if existing_permission["allowed"] == ["all_users"]: + logger.warning("The permission was not updated all addition/removal requests already match the current state.") + return + # Update permission with default (all_users) operation_logger.related_to.append(('app', permission.split(".")[0])) diff --git a/src/yunohost/tests/test_permission.py b/src/yunohost/tests/test_permission.py index a9e16cfc6..0ddda4cec 100644 --- a/src/yunohost/tests/test_permission.py +++ b/src/yunohost/tests/test_permission.py @@ -306,6 +306,17 @@ def test_permission_reset(): assert res['blog.main']['allowed'] == ["all_users"] assert set(res['blog.main']['corresponding_users']) == set(["alice", "bob"]) + +def test_permission_reset_idempotency(): + # Reset permission + user_permission_reset("blog.main") + user_permission_reset("blog.main") + + res = user_permission_list(full=True)['permissions'] + assert res['blog.main']['allowed'] == ["all_users"] + assert set(res['blog.main']['corresponding_users']) == set(["alice", "bob"]) + + # # Error on update function # From 88794805eba3ececc5bc04aac2d41b4d09241bf7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 9 Oct 2019 22:08:12 +0200 Subject: [PATCH 21/35] We probably don't need to have multiple urls per permissions ... --- data/helpers.d/setting | 62 ++++++++--------- locales/en.json | 2 +- src/yunohost/app.py | 68 ++++++++----------- src/yunohost/backup.py | 6 +- .../0011_setup_group_permission.py | 4 +- src/yunohost/permission.py | 38 ++++------- src/yunohost/tests/test_backuprestore.py | 16 ++--- src/yunohost/tests/test_permission.py | 56 +++++---------- 8 files changed, 105 insertions(+), 147 deletions(-) diff --git a/data/helpers.d/setting b/data/helpers.d/setting index b2a647993..c911c5811 100644 --- a/data/helpers.d/setting +++ b/data/helpers.d/setting @@ -232,34 +232,36 @@ ynh_webpath_register () { # Create a new permission for the app # -# example: ynh_permission_create --permission admin --urls /admin +# example: ynh_permission_create --permission admin --url /admin # -# usage: ynh_permission_create --permission "permission" [--urls "url" ["url" ...]] +# usage: ynh_permission_create --permission "permission" [--url "url"] # | arg: permission - the name for the permission (by default a permission named "main" already exist) -# | arg: urls - (optional) a list of FULL urls for the permission (e.g. domain.tld/apps/admin) -# | arg: urls - (optional) a list of URLs to specify for the permission. +# | arg: url - (optional) URL for which access will be allowed/forbidden # -# URLs are assumed to be relative to the app domain/path if they start with '/'. -# For example: -# / -> domain.tld/app -# /admin -> domain.tld/app/admin -# domain.tld/app/api -> domain.tld/app/api +# If provided, 'url' is assumed to be relative to the app domain/path if they +# start with '/'. For example: +# / -> domain.tld/app +# /admin -> domain.tld/app/admin +# domain.tld/app/api -> domain.tld/app/api # -# URLs can be treated as regexes when they start with "re:". -# For example: -# re:/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ -# re:domain.tld/app/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ +# 'url' can be later treated as a regex if it starts with "re:". +# For example: +# re:/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ +# re:domain.tld/app/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ # ynh_permission_create() { - declare -Ar args_array=( [p]=permission= [u]=urls= ) + declare -Ar args_array=( [p]=permission= [u]=url= ) local permission local urls ynh_handle_getopts_args "$@" - if [[ -n ${urls:-} ]]; then - urls=",urls=['${urls//';'/"','"}']" + if [[ -n ${url:-} ]]; then + url="'$url'" + else + url="None" fi - yunohost tools shell -c "from yunohost.permission import permission_create; permission_create('$app.$permission' ${urls:-}, sync_perm=False)" + + yunohost tools shell -c "from yunohost.permission import permission_create; permission_create('$app.$permission', url=$url, sync_perm=False)" } # Remove a permission for the app (note that when the app is removed all permission is automatically removed) @@ -277,30 +279,28 @@ ynh_permission_delete() { yunohost tools shell -c "from yunohost.permission import permission_delete; permission_delete('$app.$permission', sync_perm=False)" } -# Manage urls related to a permission +# Redefine the url associated to a permission # -# usage: ynh_permission_urls --permission "permission" --add "url" ["url" ...] --remove "url" ["url" ...] +# usage: ynh_permission_url --permission "permission" --url "url" # | arg: permission - the name for the permission (by default a permission named "main" is removed automatically when the app is removed) -# | arg: add - (optional) a list of urls to add to the permission (see permission_create for details regarding their format) -# | arg: remove - (optional) a list of urls to remove from the permission (see permission_create for details regarding their format) +# | arg: url - (optional) URL for which access will be allowed/forbidden # -ynh_permission_urls() { - declare -Ar args_array=([p]=permission= [a]=add= [r]=remove=) +ynh_permission_url() { + declare -Ar args_array=([p]=permission= [u]=url=) local permission - local add - local remove + local url ynh_handle_getopts_args "$@" - if [[ -n ${add:-} ]]; then - add=",add=['${add//';'/"','"}']" - fi - if [[ -n ${remove:-} ]]; then - remove=",remove=['${remove//';'/"','"}']" + if [[ -n ${url:-} ]]; then + url="'$url'" + else + url="None" fi - yunohost tools shell -c "from yunohost.permission import permission_urls; permission_urls('$app.$permission' ${add:-} ${remove:-})" + yunohost tools shell -c "from yunohost.permission import permission_url; permission_url('$app.$permission', url=$url)" } + # Update a permission for the app # # usage: ynh_permission_update --permission "permission" --add "group" ["group" ...] --remove "group" ["group" ...] diff --git a/locales/en.json b/locales/en.json index 3e1054069..e3911a334 100644 --- a/locales/en.json +++ b/locales/en.json @@ -268,7 +268,7 @@ "log_letsencrypt_cert_install": "Install a Let's encrypt certificate on '{}' domain", "log_permission_create": "Create permission '{}'", "log_permission_delete": "Delete permission '{}'", - "log_permission_urls": "Update urls related to permission '{}'", + "log_permission_url": "Update url related to permission '{}'", "log_selfsigned_cert_install": "Install self signed certificate on '{}' domain", "log_letsencrypt_cert_renew": "Renew '{}' Let's encrypt certificate", "log_regen_conf": "Regenerate system configurations '{}'", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 4bda9ccf6..abb4387e5 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -454,33 +454,18 @@ def app_map(app=None, raw=False, user=None): return perm_domain, perm_path - this_app_perms = {p: i for p, i in permissions.items() if p.startswith(app_id + ".") and i["urls"]} + this_app_perms = {p: i for p, i in permissions.items() if p.startswith(app_id + ".") and i["url"]} for perm_name, perm_info in this_app_perms.items(): # If we're building the map for a specific user, check the user # actually is allowed for this specific perm if user and user not in perm_info["corresponding_users"] and "visitors" not in perm_info["allowed"]: continue - if len(perm_info["urls"]) > 1 or perm_info["urls"][0].startswith("re:"): - # - # Here we have a big conceptual issue about the sso ... - # Let me take a sip of coffee and turn off the music... - # - # Let's say we have an app foo which created a permission - # 'foo.admin' and added as url "/admin" and "/api" This - # permission got defined somehow as only accessible for group - # "admins". So both "/admin" and "/api" are protected. Good! - # - # Now if we really want users in group "admins" to access those - # uris, then each users in group "admins" need to have these - # urls in the ssowat dict for this user. Which corresponds to a - # tile. To put it otherwise : in the current code of ssowat, a - # permission = a tile = a url ! - # - # We also have an issue if the url define is a regex, because + if perm_info["url"].startswith("re:"): + # Here, we have an issue if the chosen url is a regex, because # the url we want to add to the dict is going to be turned into # a clickable link (or analyzed by other parts of yunohost # code...). To put it otherwise : in the current code of ssowat, - # you can't give access a user to a regex + # you can't give access a user to a regex. # # Instead, as drafted by Josue, we could rework the ssowat logic # about how routes and their permissions are defined. So for example, @@ -498,10 +483,10 @@ def app_map(app=None, raw=False, user=None): # protected/unprotected/skipped uris and regexes and we gotta # handle / migrate all the legacy stuff somehow if we don't # want to end up with a total mess in the future idk - logger.error("Permission %s can't be added to the SSOwat configuration because it uses multiple urls and/or uses a regex url" % perm_name) + logger.error("Permission %s can't be added to the SSOwat configuration because it doesn't support regexes so far..." % perm_name) continue - perm_domain, perm_path = _sanitized_absolute_url(perm_info["urls"][0]) + perm_domain, perm_path = _sanitized_absolute_url(perm_info["url"]) if perm_name.endswith(".main"): perm_label = label @@ -535,7 +520,6 @@ def app_change_url(operation_logger, app, domain, path): """ from yunohost.hook import hook_exec, hook_callback from yunohost.domain import _normalize_domain_path, _get_conflicting_apps - from yunohost.permission import permission_urls installed = _is_installed(app) if not installed: @@ -835,7 +819,7 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback from yunohost.log import OperationLogger - from yunohost.permission import user_permission_list, permission_create, permission_urls, permission_delete, permission_sync_to_user, user_permission_update + from yunohost.permission import user_permission_list, permission_create, permission_url, permission_delete, permission_sync_to_user, user_permission_update # Fetch or extract sources if not os.path.exists(INSTALL_TMP): @@ -994,7 +978,7 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu # Initialize the main permission for the app # After the install, if apps don't have a domain and path defined, the default url '/' is removed from the permission - permission_create(app_instance_name+".main", urls=["/"]) + permission_create(app_instance_name+".main", url="/") # Execute the app install script install_retcode = 1 @@ -1088,7 +1072,7 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu domain = app_settings.get('domain', None) path = app_settings.get('path', None) if not (domain and path): - permission_urls(app_instance_name + ".main", remove=["/"], sync_perm=False) + permission_url(app_instance_name + ".main", url=None, sync_perm=False) # Migrate classic public app still using the legacy unprotected_uris if app_settings.get("unprotected_uris", None) == "/": @@ -1178,7 +1162,7 @@ def app_addaccess(apps, users=[]): """ from yunohost.permission import user_permission_update - logger.warning("/!\\ Packagers ! This app is using the legacy permission system. Please use the new helpers ynh_permission_{create,urls,update,delete} and the 'visitors' group to manage permissions.") + logger.warning("/!\\ Packagers ! This app is using the legacy permission system. Please use the new helpers ynh_permission_{create,url,update,delete} and the 'visitors' group to manage permissions.") output = {} for app in apps: @@ -1199,7 +1183,7 @@ def app_removeaccess(apps, users=[]): """ from yunohost.permission import user_permission_update - logger.warning("/!\\ Packagers ! This app is using the legacy permission system. Please use the new helpers ynh_permission_{create,urls,update,delete} and the 'visitors' group to manage permissions.") + logger.warning("/!\\ Packagers ! This app is using the legacy permission system. Please use the new helpers ynh_permission_{create,url,update,delete} and the 'visitors' group to manage permissions.") output = {} for app in apps: @@ -1219,7 +1203,7 @@ def app_clearaccess(apps): """ from yunohost.permission import user_permission_reset - logger.warning("/!\\ Packagers ! This app is using the legacy permission system. Please use the new helpers ynh_permission_{create,urls,update,delete} and the 'visitors' group to manage permissions.") + logger.warning("/!\\ Packagers ! This app is using the legacy permission system. Please use the new helpers ynh_permission_{create,url,update,delete} and the 'visitors' group to manage permissions.") output = {} for app in apps: @@ -1329,7 +1313,7 @@ def app_setting(app, key, value=None, delete=False): if key in ['redirected_urls', 'redirected_regex']: value = yaml.load(value) if key in ["unprotected_uris", "unprotected_regex", "protected_uris", "protected_regex"]: - logger.warning("/!\ Packagers ! This app is using the legacy permission system. Please delete these legacy settings and use the new helpers ynh_permission_{create,urls,update,delete} and the 'visitors' group to manage public/private access.") + logger.warning("/!\ Packagers ! This app is using the legacy permission system. Please delete these legacy settings and use the new helpers ynh_permission_{create,url,update,delete} and the 'visitors' group to manage public/private access.") app_settings[key] = value _set_app_settings(app, app_settings) @@ -1562,20 +1546,24 @@ def app_ssowatconf(): # New permission system this_app_perms = {name: info for name, info in all_permissions.items() if name.startswith(app['id'] + ".")} for perm_name, perm_info in this_app_perms.items(): + + # Ignore permissions for which there's no url defined + if not perm_info["url"]: + continue + # FIXME : gotta handle regex-urls here... meh - urls = [_sanitized_absolute_url(url) for url in perm_info["urls"]] + url = _sanitized_absolute_url(perm_info["url"]) if "visitors" in perm_info["allowed"]: - unprotected_urls += urls + unprotected_urls.append(url) # Legacy stuff : we remove now unprotected-urls that might have been declared as protected earlier... - protected_urls = [u for u in protected_urls if u not in urls] + protected_urls = [u for u in protected_urls if u != url] else: # TODO : small optimization to implement : we don't need to explictly add all the app roots - - protected_urls += urls + protected_urls.append(url) # Legacy stuff : we remove now unprotected-urls that might have been declared as protected earlier... - unprotected_urls = [u for u in unprotected_urls if u not in urls] + unprotected_urls = [u for u in unprotected_urls if u != url] for domain in domains: skipped_urls.extend([domain + '/yunohost/admin', domain + '/yunohost/api']) @@ -1585,11 +1573,13 @@ def app_ssowatconf(): skipped_regex.append("^[^/]*/%.well%-known/autoconfig/mail/config%-v1%.1%.xml.*$") - permissions_per_url = {} - for permission_name, permission_infos in all_permissions.items(): - for url in permission_infos["urls"]: - permissions_per_url[url] = permission_infos['corresponding_users'] + for perm_name, perm_info in all_permissions.items(): + # Ignore permissions for which there's no url defined + if not perm_info["url"]: + continue + permissions_per_url[perm_info["url"]] = perm_info['corresponding_users'] + conf_dict = { 'portal_domain': main_domain, diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index c28160342..dcdb1adec 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -1189,7 +1189,7 @@ class RestoreManager(): return from yunohost.user import user_group_list - from yunohost.permission import permission_create, permission_delete, user_permission_update, user_permission_list + from yunohost.permission import permission_create, permission_delete, user_permission_update, user_permission_list, permission_sync_to_user # Backup old permission for apps # We need to do that because in case of an app is installed we can't remove the permission for this app @@ -1251,7 +1251,7 @@ class RestoreManager(): for permission_name, permission_infos in old_apps_permission.items(): app_name = permission_name.split(".")[0] if _is_installed(app_name): - permission_create(permission_name, urls=permission_infos["urls"], sync_perm=False) + permission_create(permission_name, url=permission_infos["url"], sync_perm=False) user_permission_update(permission_name, remove="all_users", add=permission_infos["allowed"]) def _restore_apps(self): @@ -1362,7 +1362,7 @@ class RestoreManager(): for permission_name, permission_infos in permissions.items(): - permission_create(permission_name, urls=permission_infos.get("urls", [])) + permission_create(permission_name, url=permission_infos.get("url", None)) if "allowed" not in permission_infos: logger.warning("'allowed' key corresponding to allowed groups for permission %s not found when restoring app %s … You might have to reconfigure permissions yourself." % (permission_name, app_instance_name)) diff --git a/src/yunohost/data_migrations/0011_setup_group_permission.py b/src/yunohost/data_migrations/0011_setup_group_permission.py index de28a3ad7..880c5f54b 100644 --- a/src/yunohost/data_migrations/0011_setup_group_permission.py +++ b/src/yunohost/data_migrations/0011_setup_group_permission.py @@ -107,8 +107,8 @@ class MyMigration(Migration): path = app_setting(app, 'path') domain = app_setting(app, 'domain') - urls = "/" if domain and path else None - permission_create(app+".main", urls=urls, sync_perm=False) + url = "/" if domain and path else None + permission_create(app+".main", url=url, sync_perm=False) if permission: allowed_group = permission.split(',') user_permission_update(app+".main", remove="all_users", add=allowed_group, sync_perm=False) diff --git a/src/yunohost/permission.py b/src/yunohost/permission.py index 97e5b4122..6f9d63d69 100644 --- a/src/yunohost/permission.py +++ b/src/yunohost/permission.py @@ -73,7 +73,7 @@ def user_permission_list(short=False, full=False, ignore_system_perms=False): if full: permissions[name]["corresponding_users"] = [_ldap_path_extract(p, "uid") for p in infos.get('inheritPermission', [])] - permissions[name]["urls"] = infos.get("URL", []) + permissions[name]["url"] = infos.get("URL", [None])[0] if short: permissions = permissions.keys() @@ -260,27 +260,27 @@ def user_permission_reset(operation_logger, permission, sync_perm=True): # # The followings methods are *not* directly exposed. # They are used to create/delete the permissions (e.g. during app install/remove) -# and by some app helpers to possibly add additional permissions and tweak the urls +# and by some app helpers to possibly add additional permissions # # @is_unit_operation() -def permission_create(operation_logger, permission, urls=None, sync_perm=True): +def permission_create(operation_logger, permission, url=None, sync_perm=True): """ Create a new permission for a specific application Keyword argument: permission -- Name of the permission (e.g. mail or nextcloud or wordpress.editors) - urls -- list of URLs to specify for the permission. + url -- (optional) URL for which access will be allowed/forbidden - Urls are assumed to be relative to the app domain/path if they start with '/'. - For example: + If provided, 'url' is assumed to be relative to the app domain/path if they + start with '/'. For example: / -> domain.tld/app /admin -> domain.tld/app/admin domain.tld/app/api -> domain.tld/app/api - URLs can be later treated as regexes when they start with "re:". + 'url' can be later treated as a regex if it starts with "re:". For example: re:/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ re:domain.tld/app/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ @@ -316,8 +316,8 @@ def permission_create(operation_logger, permission, urls=None, sync_perm=True): if permission.endswith(".main"): attr_dict['groupPermission'] = ['cn=all_users,ou=groups,dc=yunohost,dc=org'] - if urls: - attr_dict['URL'] = urls + if url: + attr_dict['URL'] = url operation_logger.related_to.append(('app', permission.split(".")[0])) operation_logger.start() @@ -335,15 +335,13 @@ def permission_create(operation_logger, permission, urls=None, sync_perm=True): @is_unit_operation() -def permission_urls(operation_logger, permission, add=None, remove=None, sync_perm=True): +def permission_url(operation_logger, permission, url=None, sync_perm=True): """ Update urls related to a permission for a specific application Keyword argument: permission -- Name of the permission (e.g. mail or nextcloud or wordpress.editors) - add -- List of URLs to add (c.f. permission_create for documentation about their format) - remove -- List of URLs to remove (c.f. permission_create for documentation about their format) - + url -- (optional) URL for which access will be allowed/forbidden """ from yunohost.utils.ldap import _get_ldap_interface ldap = _get_ldap_interface() @@ -355,17 +353,9 @@ def permission_urls(operation_logger, permission, add=None, remove=None, sync_pe raise YunohostError('permission_not_found', permission=permission) # Compute new url list + old_url = existing_permission["url"] - new_urls = copy.copy(existing_permission["urls"]) - - if add: - urls_to_add = [add] if not isinstance(add, list) else add - new_urls += urls_to_add - if remove: - urls_to_remove = [remove] if not isinstance(remove, list) else remove - new_urls = [u for u in new_urls if u not in urls_to_remove] - - if set(new_urls) == set(existing_permission["urls"]): + if old_url == url: logger.warning(m18n.n('permission_update_nothing_to_do')) return existing_permission @@ -375,7 +365,7 @@ def permission_urls(operation_logger, permission, add=None, remove=None, sync_pe operation_logger.start() try: - ldap.update('cn=%s,ou=permission' % permission, {'URL': new_urls}) + ldap.update('cn=%s,ou=permission' % permission, {'URL': [url]}) except Exception as e: raise YunohostError('permission_update_failed', permission=permission, error=e) diff --git a/src/yunohost/tests/test_backuprestore.py b/src/yunohost/tests/test_backuprestore.py index cab98089b..82d3da660 100644 --- a/src/yunohost/tests/test_backuprestore.py +++ b/src/yunohost/tests/test_backuprestore.py @@ -529,11 +529,11 @@ def test_backup_and_restore_permission_app(): assert "permissions_app.main" in res assert "permissions_app.admin" in res assert "permissions_app.dev" in res - assert res['permissions_app.main']['urls'] == ["/"] - assert res['permissions_app.admin']['urls'] == ["/admin"] - assert res['permissions_app.dev']['urls'] == ["/dev"] + assert res['permissions_app.main']['url'] == "/" + assert res['permissions_app.admin']['url'] == "/admin" + assert res['permissions_app.dev']['url'] == "/dev" - assert res['permissions_app.main']['allowed'] == ["all_users"] + assert res['permissions_app.main']['allowed'] == ["visitors"] assert res['permissions_app.admin']['allowed'] == ["alice"] assert res['permissions_app.dev']['allowed'] == [] @@ -543,11 +543,11 @@ def test_backup_and_restore_permission_app(): assert "permissions_app.main" in res assert "permissions_app.admin" in res assert "permissions_app.dev" in res - assert res['permissions_app.main']['urls'] == ["/"] - assert res['permissions_app.admin']['urls'] == ["/admin"] - assert res['permissions_app.dev']['urls'] == ["/dev"] + assert res['permissions_app.main']['url'] == "/" + assert res['permissions_app.admin']['url'] == "/admin" + assert res['permissions_app.dev']['url'] == "/dev" - assert res['permissions_app.main']['allowed'] == ["all_users"] + assert res['permissions_app.main']['allowed'] == ["visitors"] assert res['permissions_app.admin']['allowed'] == ["alice"] assert res['permissions_app.dev']['allowed'] == [] diff --git a/src/yunohost/tests/test_permission.py b/src/yunohost/tests/test_permission.py index 0ddda4cec..8e536ec9e 100644 --- a/src/yunohost/tests/test_permission.py +++ b/src/yunohost/tests/test_permission.py @@ -6,7 +6,7 @@ from yunohost.app import app_install, app_remove, app_change_url, app_list, app_ from yunohost.user import user_list, user_info, user_create, user_delete, user_update, \ user_group_list, user_group_create, user_group_delete, user_group_update, user_group_info from yunohost.permission import user_permission_update, user_permission_list, user_permission_reset, \ - permission_create, permission_urls, permission_delete + permission_create, permission_delete, permission_url from yunohost.domain import _get_maindomain from yunohost.utils.error import YunohostError @@ -31,7 +31,7 @@ def setup_function(function): user_create("alice", "Alice", "White", "alice@" + maindomain, dummy_password) user_create("bob", "Bob", "Snow", "bob@" + maindomain, dummy_password) - permission_create("wiki.main", urls=["/"], sync_perm=False) + permission_create("wiki.main", url="/", sync_perm=False) permission_create("blog.main", sync_perm=False) user_permission_update("blog.main", remove="all_users", add="alice") @@ -202,7 +202,7 @@ def test_permission_list(): assert res['blog.main']['allowed'] == ["alice"] assert set(res['wiki.main']['corresponding_users']) == set(["alice", "bob"]) assert res['blog.main']['corresponding_users'] == ["alice"] - assert res['wiki.main']['urls'] == ["/"] + assert res['wiki.main']['url'] == "/" # # Create - Remove functions @@ -333,41 +333,19 @@ def test_permission_update_permission_that_doesnt_exist(): with pytest.raises(YunohostError): user_permission_update("doesnt.exist", add="alice") - # Permission url management -def test_permission_add_url(): - permission_urls("blog.main", add=["/testA"]) +def test_permission_redefine_url(): + permission_url("blog.main", url="/pwet") res = user_permission_list(full=True)['permissions'] - assert res["blog.main"]["urls"] == ["/testA"] - -def test_permission_add_another_url(): - permission_urls("wiki.main", add=["/testA"]) - - res = user_permission_list(full=True)['permissions'] - assert set(res["wiki.main"]["urls"]) == set(["/", "/testA"]) + assert res["blog.main"]["url"] == "/pwet" def test_permission_remove_url(): - permission_urls("wiki.main", remove=["/"]) + permission_url("blog.main", url=None) res = user_permission_list(full=True)['permissions'] - assert res["wiki.main"]["urls"] == [] - -def test_permission_add_url_already_added(): - res = user_permission_list(full=True)['permissions'] - assert res["wiki.main"]["urls"] == ["/"] - - permission_urls("wiki.main", add=["/"]) - - res = user_permission_list(full=True)['permissions'] - assert res["wiki.main"]["urls"] == ["/"] - -def test_permission_remove_url_not_added(): - permission_urls("wiki.main", remove=["/doesnt_exist"]) - - res = user_permission_list(full=True)['permissions'] - assert res['wiki.main']['urls'] == ["/"] + assert res["blog.main"]["url"] is None # # Application interaction @@ -381,9 +359,9 @@ def test_permission_app_install(): assert "permissions_app.main" in res assert "permissions_app.admin" in res assert "permissions_app.dev" in res - assert res['permissions_app.main']['urls'] == ["/"] - assert res['permissions_app.admin']['urls'] == ["/admin"] - assert res['permissions_app.dev']['urls'] == ["/dev"] + assert res['permissions_app.main']['url'] == "/" + assert res['permissions_app.admin']['url'] == "/admin" + assert res['permissions_app.dev']['url'] == "/dev" assert res['permissions_app.main']['allowed'] == ["all_users"] assert set(res['permissions_app.main']['corresponding_users']) == set(["alice", "bob"]) @@ -416,16 +394,16 @@ def test_permission_app_change_url(): # FIXME : should rework this test to look for differences in the generated app map / app tiles ... res = user_permission_list(full=True)['permissions'] - assert res['permissions_app.main']['urls'] == ["/"] - assert res['permissions_app.admin']['urls'] == ["/admin"] - assert res['permissions_app.dev']['urls'] == ["/dev"] + assert res['permissions_app.main']['url'] == "/" + assert res['permissions_app.admin']['url'] == "/admin" + assert res['permissions_app.dev']['url'] == "/dev" app_change_url("permissions_app", maindomain, "/newchangeurl") res = user_permission_list(full=True)['permissions'] - assert res['permissions_app.main']['urls'] == ["/"] - assert res['permissions_app.admin']['urls'] == ["/admin"] - assert res['permissions_app.dev']['urls'] == ["/dev"] + assert res['permissions_app.main']['url'] == "/" + assert res['permissions_app.admin']['url'] == "/admin" + assert res['permissions_app.dev']['url'] == "/dev" def test_permission_app_propagation_on_ssowat(): From 2617fd2487d8940309b7a19cb3011985eff3a327 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 9 Oct 2019 22:11:19 +0200 Subject: [PATCH 22/35] Fix issues related to regerating ssowat conf while hacking permissions... --- src/yunohost/backup.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index dcdb1adec..90f795ea5 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -1245,14 +1245,17 @@ class RestoreManager(): # Remove all permission for all app which is still in the LDAP for permission_name in user_permission_list(ignore_system_perms=True)["permissions"].keys(): - permission_delete(permission_name, force=True) + permission_delete(permission_name, force=True, sync_perm=False) # Restore permission for the app which is installed for permission_name, permission_infos in old_apps_permission.items(): app_name = permission_name.split(".")[0] if _is_installed(app_name): permission_create(permission_name, url=permission_infos["url"], sync_perm=False) - user_permission_update(permission_name, remove="all_users", add=permission_infos["allowed"]) + user_permission_update(permission_name, remove="all_users", add=permission_infos["allowed"], sync_perm=False) + + permission_sync_to_user() + def _restore_apps(self): """Restore all apps targeted""" @@ -1290,7 +1293,7 @@ class RestoreManager(): restore_app_failed -- Raised if the restore bash script failed """ from yunohost.user import user_group_list - from yunohost.permission import permission_create, permission_delete, user_permission_list, user_permission_update + from yunohost.permission import permission_create, permission_delete, user_permission_list, user_permission_update, permission_sync_to_user def copytree(src, dst, symlinks=False, ignore=None): for item in os.listdir(src): @@ -1362,7 +1365,7 @@ class RestoreManager(): for permission_name, permission_infos in permissions.items(): - permission_create(permission_name, url=permission_infos.get("url", None)) + permission_create(permission_name, url=permission_infos.get("url", None), sync_perm=False) if "allowed" not in permission_infos: logger.warning("'allowed' key corresponding to allowed groups for permission %s not found when restoring app %s … You might have to reconfigure permissions yourself." % (permission_name, app_instance_name)) @@ -1370,7 +1373,9 @@ class RestoreManager(): should_be_allowed = [g for g in permission_infos["allowed"] if g in existing_groups] current_allowed = user_permission_list()["permissions"][permission_name]["allowed"] if should_be_allowed != current_allowed: - user_permission_update(permission_name, remove=current_allowed, add=should_be_allowed) + user_permission_update(permission_name, remove=current_allowed, add=should_be_allowed, sync_perm=False) + + permission_sync_to_user() os.remove('%s/permissions.yml' % app_settings_new_path) else: From c315df926999376fd79b81dd32072d8d3f06e4a9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 9 Oct 2019 22:48:47 +0200 Subject: [PATCH 23/35] Wokay, getting tired of breaking the entire permission/group ecosystem because of bugs when developing. --- src/yunohost/app.py | 3 +++ src/yunohost/user.py | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index abb4387e5..75bf12f3d 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -433,6 +433,9 @@ def app_map(app=None, raw=False, user=None): continue # Users must at least have access to the main permission to have access to extra permissions if user: + if not app_id + ".main" in permissions: + logger.warning("Uhoh, no main permission was found for app %s ... sounds like an app was only partially removed due to another bug :/" % app_id) + continue main_perm = permissions[app_id + ".main"] if user not in main_perm["corresponding_users"] and "visitors" not in main_perm["allowed"]: continue diff --git a/src/yunohost/user.py b/src/yunohost/user.py index f4e550230..72aa36184 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -268,7 +268,12 @@ def user_delete(operation_logger, username, purge=False): # remove the member from the group if username != group and username in infos["members"]: user_group_update(group, remove=username, sync_perm=False) - user_group_delete(username, force=True, sync_perm=True) + + # Delete primary group if it exists (why wouldnt it exists ? because some + # epic bug happened somewhere else and only a partial removal was + # performed...) + if username in user_group_list()['groups'].keys(): + user_group_delete(username, force=True, sync_perm=True) ldap = _get_ldap_interface() try: From e7d1cc5f9449f77bf575c82f9faae5f2f2168b41 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 9 Oct 2019 22:55:06 +0200 Subject: [PATCH 24/35] Allow to specify right away what groups to allow for a permission when creating it --- data/helpers.d/setting | 16 +++++++++++----- src/yunohost/app.py | 2 +- src/yunohost/backup.py | 11 ++++------- .../0011_setup_group_permission.py | 8 +++++--- src/yunohost/permission.py | 16 ++++++++++++++-- src/yunohost/tests/test_permission.py | 9 +++++++++ 6 files changed, 44 insertions(+), 18 deletions(-) diff --git a/data/helpers.d/setting b/data/helpers.d/setting index c911c5811..a8d2919a4 100644 --- a/data/helpers.d/setting +++ b/data/helpers.d/setting @@ -232,11 +232,12 @@ ynh_webpath_register () { # Create a new permission for the app # -# example: ynh_permission_create --permission admin --url /admin +# example: ynh_permission_create --permission admin --url /admin --allowed alice bob # -# usage: ynh_permission_create --permission "permission" [--url "url"] +# usage: ynh_permission_create --permission "permission" [--url "url"] [--allowed group1 group2] # | arg: permission - the name for the permission (by default a permission named "main" already exist) # | arg: url - (optional) URL for which access will be allowed/forbidden +# | arg: allowed - (optional) A list of group/user to allow for the permission # # If provided, 'url' is assumed to be relative to the app domain/path if they # start with '/'. For example: @@ -250,9 +251,10 @@ ynh_webpath_register () { # re:domain.tld/app/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ # ynh_permission_create() { - declare -Ar args_array=( [p]=permission= [u]=url= ) + declare -Ar args_array=( [p]=permission= [u]=url= [a]=allowed= ) local permission - local urls + local url + local allowed ynh_handle_getopts_args "$@" if [[ -n ${url:-} ]]; then @@ -261,7 +263,11 @@ ynh_permission_create() { url="None" fi - yunohost tools shell -c "from yunohost.permission import permission_create; permission_create('$app.$permission', url=$url, sync_perm=False)" + if [[ -n ${allowed:-} ]]; then + allowed=",allowed=['${allowed//';'/"','"}']" + fi + + yunohost tools shell -c "from yunohost.permission import permission_create; permission_create('$app.$permission', url=$url ${allowed:-} , sync_perm=False)" } # Remove a permission for the app (note that when the app is removed all permission is automatically removed) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 75bf12f3d..7235535cd 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -981,7 +981,7 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu # Initialize the main permission for the app # After the install, if apps don't have a domain and path defined, the default url '/' is removed from the permission - permission_create(app_instance_name+".main", url="/") + permission_create(app_instance_name+".main", url="/", allowed=["all_users"]) # Execute the app install script install_retcode = 1 diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 90f795ea5..c57ab6685 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -1251,8 +1251,7 @@ class RestoreManager(): for permission_name, permission_infos in old_apps_permission.items(): app_name = permission_name.split(".")[0] if _is_installed(app_name): - permission_create(permission_name, url=permission_infos["url"], sync_perm=False) - user_permission_update(permission_name, remove="all_users", add=permission_infos["allowed"], sync_perm=False) + permission_create(permission_name, url=permission_infos["url"], allowed=permission_infos["allowed"], sync_perm=False) permission_sync_to_user() @@ -1365,15 +1364,13 @@ class RestoreManager(): for permission_name, permission_infos in permissions.items(): - permission_create(permission_name, url=permission_infos.get("url", None), sync_perm=False) - if "allowed" not in permission_infos: logger.warning("'allowed' key corresponding to allowed groups for permission %s not found when restoring app %s … You might have to reconfigure permissions yourself." % (permission_name, app_instance_name)) + should_be_allowed = ["all_users"] else: should_be_allowed = [g for g in permission_infos["allowed"] if g in existing_groups] - current_allowed = user_permission_list()["permissions"][permission_name]["allowed"] - if should_be_allowed != current_allowed: - user_permission_update(permission_name, remove=current_allowed, add=should_be_allowed, sync_perm=False) + + permission_create(permission_name, url=permission_infos.get("url", None), allowed=should_be_allowed, sync_perm=False) permission_sync_to_user() diff --git a/src/yunohost/data_migrations/0011_setup_group_permission.py b/src/yunohost/data_migrations/0011_setup_group_permission.py index 880c5f54b..3114817b9 100644 --- a/src/yunohost/data_migrations/0011_setup_group_permission.py +++ b/src/yunohost/data_migrations/0011_setup_group_permission.py @@ -108,10 +108,12 @@ class MyMigration(Migration): domain = app_setting(app, 'domain') url = "/" if domain and path else None - permission_create(app+".main", url=url, sync_perm=False) if permission: - allowed_group = permission.split(',') - user_permission_update(app+".main", remove="all_users", add=allowed_group, sync_perm=False) + allowed_groups = permission.split(',') + else: + allowed_groups = ["all_users"] + permission_create(app+".main", url=url, allowed=allowed_groups, sync_perm=False) + app_setting(app, 'allowed_users', delete=True) # Migrate classic public app still using the legacy unprotected_uris diff --git a/src/yunohost/permission.py b/src/yunohost/permission.py index 6f9d63d69..426ecd10f 100644 --- a/src/yunohost/permission.py +++ b/src/yunohost/permission.py @@ -266,13 +266,14 @@ def user_permission_reset(operation_logger, permission, sync_perm=True): @is_unit_operation() -def permission_create(operation_logger, permission, url=None, sync_perm=True): +def permission_create(operation_logger, permission, url=None, allowed=None, sync_perm=True): """ Create a new permission for a specific application Keyword argument: permission -- Name of the permission (e.g. mail or nextcloud or wordpress.editors) url -- (optional) URL for which access will be allowed/forbidden + allowed -- (optional) A list of group/user to allow for the permission If provided, 'url' is assumed to be relative to the app domain/path if they start with '/'. For example: @@ -286,6 +287,7 @@ def permission_create(operation_logger, permission, url=None, sync_perm=True): re:domain.tld/app/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ """ + from yunohost.user import user_group_list from yunohost.utils.ldap import _get_ldap_interface ldap = _get_ldap_interface() @@ -312,8 +314,18 @@ def permission_create(operation_logger, permission, url=None, sync_perm=True): 'gidNumber': gid, } + # If who should be allowed is explicitly provided, use this info + if allowed: + if not isinstance(allowed, list): + allowed = [allowed] + # (though first we validate that the targets actually exist) + all_existing_groups = user_group_list()['groups'].keys() + for g in allowed: + if g not in all_existing_groups: + raise YunohostError('group_unknown', group=g) + attr_dict['groupPermission'] = ['cn=%s,ou=groups,dc=yunohost,dc=org' % g for g in allowed] # For main permission, we add all users by default - if permission.endswith(".main"): + elif permission.endswith(".main"): attr_dict['groupPermission'] = ['cn=all_users,ou=groups,dc=yunohost,dc=org'] if url: diff --git a/src/yunohost/tests/test_permission.py b/src/yunohost/tests/test_permission.py index 8e536ec9e..5e1246793 100644 --- a/src/yunohost/tests/test_permission.py +++ b/src/yunohost/tests/test_permission.py @@ -226,6 +226,15 @@ def test_permission_create_extra(): assert "all_users" not in res['site.test']['allowed'] assert res['site.test']['corresponding_users'] == [] + +def test_permission_create_with_allowed(): + permission_create("site.test", allowed=["alice"]) + + res = user_permission_list(full=True)['permissions'] + assert "site.test" in res + assert res['site.test']['allowed'] == ["alice"] + + def test_permission_delete(): permission_delete("wiki.main", force=True) From 4bdcfb4373bfc0ce44931df209fb5d949a3d3768 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 9 Oct 2019 23:16:07 +0200 Subject: [PATCH 25/35] Implement / fix i18n strings --- locales/en.json | 3 +++ .../data_migrations/0011_setup_group_permission.py | 2 +- src/yunohost/permission.py | 13 ++++--------- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/locales/en.json b/locales/en.json index e3911a334..c6a6a440e 100644 --- a/locales/en.json +++ b/locales/en.json @@ -415,9 +415,12 @@ "permission_already_allowed": "Group '{group}' already has permission '{permission}' enabled'", "permission_already_disallowed": "Group '{group}' already has permission '{permission}' disabled'", "permission_already_exist": "Permission '{permission}' already exists", + "permission_already_up_to_date": "The permission was not updated because the addition/removal requests already match the current state.", "permission_cannot_remove_main": "Removing a main permission is not allowed", "permission_created": "Permission '{permission:s}' created", "permission_creation_failed": "Could not create permission '{permission}': {error}", + "permission_currently_allowed_for_visitors": "This permission is currently granted to visitors in addition to other groups. You probably want to either remove the 'visitors' permission or remove the other groups it is currently granted to.", + "permission_currently_allowed_for_all_users": "This permission is currently granted to all users in addition to other groups. You probably want to either remove the 'all_users' permission or remove the other groups it is currently granted to.", "permission_deleted": "Permission '{permission:s}' deleted", "permission_deletion_failed": "Could not delete permission '{permission}': {error}", "permission_not_found": "Permission '{permission:s}' not found", diff --git a/src/yunohost/data_migrations/0011_setup_group_permission.py b/src/yunohost/data_migrations/0011_setup_group_permission.py index 3114817b9..9ba2268d9 100644 --- a/src/yunohost/data_migrations/0011_setup_group_permission.py +++ b/src/yunohost/data_migrations/0011_setup_group_permission.py @@ -132,7 +132,7 @@ class MyMigration(Migration): ldap_regen_conf_status = regen_conf(names=['slapd'], dry_run=True) # By this we check if the have been customized if ldap_regen_conf_status and ldap_regen_conf_status['slapd']['pending']: - logger.warning("migration_0011_slapd_config_will_be_overwritten", conf_backup_folder=BACKUP_CONF_DIR) + logger.warning(m18n.n("migration_0011_slapd_config_will_be_overwritten", conf_backup_folder=BACKUP_CONF_DIR)) # Backup LDAP and the apps settings before to do the migration logger.info(m18n.n("migration_0011_backup_before_migration")) diff --git a/src/yunohost/permission.py b/src/yunohost/permission.py index 426ecd10f..4cfbc214f 100644 --- a/src/yunohost/permission.py +++ b/src/yunohost/permission.py @@ -144,18 +144,13 @@ def user_permission_update(operation_logger, permission, add=None, remove=None, if len(new_allowed_groups) > 1: if "all_users" in new_allowed_groups: - # FIXME : i18n - # FIXME : write a better explanation ? - logger.warning("This permission is currently granted to all users in addition to other groups. You probably want to either remove the 'all_users' permission or remove the other groups it is currently granted to.") + logger.warning(m18n.n("permission_currently_allowed_for_all_users")) if "visitors" in new_allowed_groups: - # FIXME : i18n - # FIXME : write a better explanation ? - logger.warning("This permission is currently granted to visitors in addition to other groups. You probably want to either remove the 'visitors' permission or remove the other groups it is currently granted to.") + logger.warning(m18n.n("permission_currently_allowed_for_visitors")) # Don't update LDAP if we update exactly the same values if set(new_allowed_groups) == set(current_allowed_groups): - # FIXME : i18n - logger.warning("The permission was not updated all addition/removal requests already match the current state.") + logger.warning("permission_already_up_to_date") return # Commit the new allowed group list @@ -218,7 +213,7 @@ def user_permission_reset(operation_logger, permission, sync_perm=True): raise YunohostError('permission_not_found', permission=permission) if existing_permission["allowed"] == ["all_users"]: - logger.warning("The permission was not updated all addition/removal requests already match the current state.") + logger.warning(m18n.n("permission_already_up_to_date")) return # Update permission with default (all_users) From e4163136bbe6c7eff26102767400c0a99cb702c0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 9 Oct 2019 23:40:50 +0200 Subject: [PATCH 26/35] Don't attempt to delete the 'visitors' group during user/group tests --- src/yunohost/tests/test_user-group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/tests/test_user-group.py b/src/yunohost/tests/test_user-group.py index 30bdeb017..53fded94c 100644 --- a/src/yunohost/tests/test_user-group.py +++ b/src/yunohost/tests/test_user-group.py @@ -14,7 +14,7 @@ def clean_user_groups(): user_delete(u) for g in user_group_list()['groups']: - if g != "all_users": + if g not in ["all_users", "visitors"]: user_group_delete(g) def setup_function(function): From e48036a0829ed2754d2c39e033c78dd8b6d07c84 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 10 Oct 2019 00:05:20 +0200 Subject: [PATCH 27/35] Fix test about private app installs --- src/yunohost/tests/test_apps.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/yunohost/tests/test_apps.py b/src/yunohost/tests/test_apps.py index fc44ef105..fb2f13c3f 100644 --- a/src/yunohost/tests/test_apps.py +++ b/src/yunohost/tests/test_apps.py @@ -119,10 +119,10 @@ def app_is_exposed_on_http(domain, path, message_in_page): return False -def install_legacy_app(domain, path): +def install_legacy_app(domain, path, public=True): app_install("./tests/apps/legacy_app_ynh", - args="domain=%s&path=%s" % (domain, path), + args="domain=%s&path=%s&is_public=%s" % (domain, path, 1 if public else 0), force=True) @@ -180,13 +180,7 @@ def test_legacy_app_install_secondary_domain_on_root(secondary_domain): def test_legacy_app_install_private(secondary_domain): - install_legacy_app(secondary_domain, "/legacy") - - settings = open("/etc/yunohost/apps/legacy_app/settings.yml", "r").read() - new_settings = settings.replace("\nunprotected_uris: /", "") - assert new_settings != settings - open("/etc/yunohost/apps/legacy_app/settings.yml", "w").write(new_settings) - app_ssowatconf() + install_legacy_app(secondary_domain, "/legacy", public=False) assert app_is_installed(secondary_domain, "legacy_app") assert not app_is_exposed_on_http(secondary_domain, "/legacy", "This is a dummy app") From 08d97172369f0bb959398e539aba880a95dd7330 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 14 Oct 2019 20:26:59 +0200 Subject: [PATCH 28/35] Improve test accuracy for apps --- src/yunohost/tests/test_apps.py | 78 ++++++++++++++++----------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/src/yunohost/tests/test_apps.py b/src/yunohost/tests/test_apps.py index fb2f13c3f..5a6db43c9 100644 --- a/src/yunohost/tests/test_apps.py +++ b/src/yunohost/tests/test_apps.py @@ -4,6 +4,8 @@ import pytest import shutil import requests +from conftest import message, raiseYunohostError + from moulinette import m18n from moulinette.utils.filesystem import mkdir @@ -113,9 +115,9 @@ def app_is_not_installed(domain, app): def app_is_exposed_on_http(domain, path, message_in_page): try: - r = requests.get("http://127.0.0.1" + path + "/", headers={"Host": domain}, timeout=10) + r = requests.get("http://127.0.0.1" + path + "/", headers={"Host": domain}, timeout=10, verify=False) return r.status_code == 200 and message_in_page in r.text - except Exception: + except Exception as e: return False @@ -190,11 +192,11 @@ def test_legacy_app_install_private(secondary_domain): assert app_is_not_installed(secondary_domain, "legacy_app") -def test_legacy_app_install_unknown_domain(): +def test_legacy_app_install_unknown_domain(mocker): with pytest.raises(YunohostError): - install_legacy_app("whatever.nope", "/legacy") - # TODO check error message + with message(mocker, "app_argument_invalid"): + install_legacy_app("whatever.nope", "/legacy") assert app_is_not_installed("whatever.nope", "legacy_app") @@ -221,55 +223,51 @@ def test_legacy_app_install_multiple_instances(secondary_domain): assert app_is_not_installed(secondary_domain, "legacy_app__2") -def test_legacy_app_install_path_unavailable(secondary_domain): +def test_legacy_app_install_path_unavailable(mocker, secondary_domain): # These will be removed in teardown install_legacy_app(secondary_domain, "/legacy") with pytest.raises(YunohostError): - install_legacy_app(secondary_domain, "/") - # TODO check error message + with message(mocker, "app_location_unavailable"): + install_legacy_app(secondary_domain, "/") assert app_is_installed(secondary_domain, "legacy_app") assert app_is_not_installed(secondary_domain, "legacy_app__2") -def test_legacy_app_install_bad_args(): - - with pytest.raises(YunohostError): - install_legacy_app("this.domain.does.not.exists", "/legacy") - - -def test_legacy_app_install_with_nginx_down(secondary_domain): +def test_legacy_app_install_with_nginx_down(mocker, secondary_domain): os.system("systemctl stop nginx") - with pytest.raises(YunohostError): + with raiseYunohostError(mocker, "app_action_cannot_be_ran_because_required_services_down"): install_legacy_app(secondary_domain, "/legacy") -def test_legacy_app_failed_install(secondary_domain): +def test_legacy_app_failed_install(mocker, secondary_domain): # This will conflict with the folder that the app # attempts to create, making the install fail mkdir("/var/www/legacy_app/", 0o750) with pytest.raises(YunohostError): - install_legacy_app(secondary_domain, "/legacy") - # TODO check error message + with message(mocker, 'app_install_script_failed'): + install_legacy_app(secondary_domain, "/legacy") assert app_is_not_installed(secondary_domain, "legacy_app") -def test_legacy_app_failed_remove(secondary_domain): +def test_legacy_app_failed_remove(mocker, secondary_domain): install_legacy_app(secondary_domain, "/legacy") # The remove script runs with set -eu and attempt to remove this # file without -f, so will fail if it's not there ;) os.remove("/etc/nginx/conf.d/%s.d/%s.conf" % (secondary_domain, "legacy_app")) - with pytest.raises(YunohostError): - app_remove("legacy") + + # TODO / FIXME : can't easily validate that 'app_not_properly_removed' + # is triggered for weird reasons ... + app_remove("legacy_app") # # Well here, we hit the classical issue where if an app removal script @@ -286,59 +284,61 @@ def test_full_domain_app(secondary_domain): assert app_is_exposed_on_http(secondary_domain, "/", "This is a dummy app") -def test_full_domain_app_with_conflicts(secondary_domain): +def test_full_domain_app_with_conflicts(mocker, secondary_domain): install_legacy_app(secondary_domain, "/legacy") - # TODO : once #808 is merged, add test that the message raised is 'app_full_domain_unavailable' - with pytest.raises(YunohostError): + with raiseYunohostError(mocker, "app_full_domain_unavailable"): install_full_domain_app(secondary_domain) -def test_systemfuckedup_during_app_install(secondary_domain): +def test_systemfuckedup_during_app_install(mocker, secondary_domain): with pytest.raises(YunohostError): - install_break_yo_system(secondary_domain, breakwhat="install") - os.system("nginx -t") - os.system("systemctl status nginx") + with message(mocker, "app_install_failed"): + with message(mocker, 'app_action_broke_system'): + install_break_yo_system(secondary_domain, breakwhat="install") assert app_is_not_installed(secondary_domain, "break_yo_system") -def test_systemfuckedup_during_app_remove(secondary_domain): +def test_systemfuckedup_during_app_remove(mocker, secondary_domain): install_break_yo_system(secondary_domain, breakwhat="remove") with pytest.raises(YunohostError): - app_remove("break_yo_system") - os.system("nginx -t") - os.system("systemctl status nginx") + with message(mocker, 'app_action_broke_system'): + with message(mocker, 'app_removed'): + app_remove("break_yo_system") assert app_is_not_installed(secondary_domain, "break_yo_system") -def test_systemfuckedup_during_app_install_and_remove(secondary_domain): +def test_systemfuckedup_during_app_install_and_remove(mocker, secondary_domain): with pytest.raises(YunohostError): - install_break_yo_system(secondary_domain, breakwhat="everything") + with message(mocker, "app_install_failed"): + with message(mocker, 'app_action_broke_system'): + install_break_yo_system(secondary_domain, breakwhat="everything") assert app_is_not_installed(secondary_domain, "break_yo_system") -def test_systemfuckedup_during_app_upgrade(secondary_domain): +def test_systemfuckedup_during_app_upgrade(mocker, secondary_domain): install_break_yo_system(secondary_domain, breakwhat="upgrade") with pytest.raises(YunohostError): - app_upgrade("break_yo_system", file="./tests/apps/break_yo_system_ynh") + with message(mocker, 'app_action_broke_system'): + app_upgrade("break_yo_system", file="./tests/apps/break_yo_system_ynh") -def test_failed_multiple_app_upgrade(secondary_domain): +def test_failed_multiple_app_upgrade(mocker, secondary_domain): install_legacy_app(secondary_domain, "/legacy") install_break_yo_system(secondary_domain, breakwhat="upgrade") - with pytest.raises(YunohostError): + with raiseYunohostError(mocker, 'app_not_upgraded'): app_upgrade(["break_yo_system", "legacy_app"], file={"break_yo_system": "./tests/apps/break_yo_system_ynh", "legacy": "./tests/apps/legacy_app_ynh"}) From 7d0119ade48e9627be25e5857f9d0ff91bd65747 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 15 Oct 2019 01:06:04 +0200 Subject: [PATCH 29/35] Fix backup info.json format... --- src/yunohost/backup.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index c28160342..4bf8d8afc 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -602,10 +602,10 @@ class BackupManager(): env=env_dict, chdir=self.work_dir) - ret_succeed = {hook: {path:result["state"] for path, result in infos.items()} + ret_succeed = {hook: [path for path, result in infos.items() if result["state"] == "succeed"] for hook, infos in ret.items() if any(result["state"] == "succeed" for result in infos.values())} - ret_failed = {hook: {path:result["state"] for path, result in infos.items.items()} + ret_failed = {hook: [path for path, result in infos.items.items() if result["state"] == "failed"] for hook, infos in ret.items() if any(result["state"] == "failed" for result in infos.values())} @@ -2371,6 +2371,13 @@ def backup_info(name, with_details=False, human_readable=False): if "size_details" in info.keys(): for category in ["apps", "system"]: for name, key_info in info[category].items(): + + # Stupid legacy fix for weird format between 3.5 and 3.6 + if isinstance(key_info, dict): + key_info = key_info.keys() + + info[category][name] = key_info = {"paths": key_info} + if name in info["size_details"][category].keys(): key_info["size"] = info["size_details"][category][name] if human_readable: From 6dc720f3cfbf5945d89614587f6333038d8dffa6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 15 Oct 2019 02:36:12 +0200 Subject: [PATCH 30/35] [yolo] Use read_json / write_to_json helpers to read/write ssowat conf.json.persistent --- locales/en.json | 2 -- src/yunohost/app.py | 22 +++++++++------------- src/yunohost/tools.py | 16 ++++------------ src/yunohost/user.py | 21 +++++++++------------ 4 files changed, 22 insertions(+), 39 deletions(-) diff --git a/locales/en.json b/locales/en.json index a341a6b4f..cc73d658a 100644 --- a/locales/en.json +++ b/locales/en.json @@ -508,8 +508,6 @@ "service_unknown": "Unknown service '{service:s}'", "ssowat_conf_generated": "SSOwat configuration generated", "ssowat_conf_updated": "SSOwat configuration updated", - "ssowat_persistent_conf_read_error": "Could not read persistent SSOwat configuration: {error:s}. Edit /etc/ssowat/conf.json.persistent file to fix the JSON syntax", - "ssowat_persistent_conf_write_error": "Could not save persistent SSOwat configuration: {error:s}. Edit /etc/ssowat/conf.json.persistent file to fix the JSON syntax", "system_upgraded": "System upgraded", "system_username_exists": "Username already exists in the list of system users", "this_action_broke_dpkg": "This action broke dpkg/APT (the system package managers)… You can try to solve this issue by connecting through SSH and running `sudo dpkg --configure -a`.", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index f0b4d3c25..8ac2bcb11 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -41,7 +41,7 @@ from datetime import datetime from moulinette import msignals, m18n, msettings from moulinette.utils.log import getActionLogger -from moulinette.utils.filesystem import read_json, read_toml +from moulinette.utils.filesystem import read_json, read_toml, write_to_json from yunohost.service import service_log, service_status, _run_service_command from yunohost.utils import packages @@ -1233,25 +1233,21 @@ def app_makedefault(operation_logger, app, domain=None): raise YunohostError('app_make_default_location_already_used', app=app, domain=app_domain, other_app=app_map(raw=True)[domain]["/"]["id"]) - try: - with open('/etc/ssowat/conf.json.persistent') as json_conf: - ssowat_conf = json.loads(str(json_conf.read())) - except ValueError as e: - raise YunohostError('ssowat_persistent_conf_read_error', error=e) - except IOError: + # TODO / FIXME : current trick is to add this to conf.json.persisten + # This is really not robust and should be improved + # e.g. have a flag in /etc/yunohost/apps/$app/ to say that this is the + # default app or idk... + if not os.path.exists('/etc/ssowat/conf.json.persistent'): ssowat_conf = {} + else: + ssowat_conf = read_json('/etc/ssowat/conf.json.persistent') if 'redirected_urls' not in ssowat_conf: ssowat_conf['redirected_urls'] = {} ssowat_conf['redirected_urls'][domain + '/'] = app_domain + app_path - try: - with open('/etc/ssowat/conf.json.persistent', 'w+') as f: - json.dump(ssowat_conf, f, sort_keys=True, indent=4) - except IOError as e: - raise YunohostError('ssowat_persistent_conf_write_error', error=e) - + write_to_json('/etc/ssowat/conf.json.persistent', ssowat_conf) os.system('chmod 644 /etc/ssowat/conf.json.persistent') logger.success(m18n.n('ssowat_conf_updated')) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 64689fe0c..679bf1190 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -350,25 +350,17 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, os.system('hostname yunohost.yunohost.org') # Add a temporary SSOwat rule to redirect SSO to admin page - try: - with open('/etc/ssowat/conf.json.persistent') as json_conf: - ssowat_conf = json.loads(str(json_conf.read())) - except ValueError as e: - raise YunohostError('ssowat_persistent_conf_read_error', error=str(e)) - except IOError: + if not os.path.exists('/etc/ssowat/conf.json.persistent'): ssowat_conf = {} + else: + ssowat_conf = read_json('/etc/ssowat/conf.json.persistent') if 'redirected_urls' not in ssowat_conf: ssowat_conf['redirected_urls'] = {} ssowat_conf['redirected_urls']['/'] = domain + '/yunohost/admin' - try: - with open('/etc/ssowat/conf.json.persistent', 'w+') as f: - json.dump(ssowat_conf, f, sort_keys=True, indent=4) - except IOError as e: - raise YunohostError('ssowat_persistent_conf_write_error', error=str(e)) - + write_to_json('/etc/ssowat/conf.json.persistent', ssowat_conf) os.system('chmod 644 /etc/ssowat/conf.json.persistent') # Create SSL CA diff --git a/src/yunohost/user.py b/src/yunohost/user.py index fe27492f4..e12cccb9b 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -35,8 +35,10 @@ import subprocess import copy from moulinette import m18n -from yunohost.utils.error import YunohostError from moulinette.utils.log import getActionLogger +from moulinette.utils.filesystem import read_json, write_to_json, read_yaml, write_to_yaml + +from yunohost.utils.error import YunohostError from yunohost.service import service_status from yunohost.log import is_unit_operation @@ -195,21 +197,16 @@ def user_create(operation_logger, username, firstname, lastname, mail, password, attr_dict['mail'] = [attr_dict['mail']] + aliases # If exists, remove the redirection from the SSO - try: - with open('/etc/ssowat/conf.json.persistent') as json_conf: - ssowat_conf = json.loads(str(json_conf.read())) - except ValueError as e: - raise YunohostError('ssowat_persistent_conf_read_error', error=str(e)) - except IOError: + if not os.path.exists('/etc/ssowat/conf.json.persistent'): ssowat_conf = {} + else: + ssowat_conf = read_json('/etc/ssowat/conf.json.persistent') if 'redirected_urls' in ssowat_conf and '/' in ssowat_conf['redirected_urls']: del ssowat_conf['redirected_urls']['/'] - try: - with open('/etc/ssowat/conf.json.persistent', 'w+') as f: - json.dump(ssowat_conf, f, sort_keys=True, indent=4) - except IOError as e: - raise YunohostError('ssowat_persistent_conf_write_error', error=str(e)) + + write_to_json('/etc/ssowat/conf.json.persistent', ssowat_conf) + os.system('chmod 644 /etc/ssowat/conf.json.persistent') try: ldap.add('uid=%s,ou=users' % username, attr_dict) From 4def4dfa6a7cef4137498eaece55b4732f0559a8 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 15 Oct 2019 14:54:52 +0200 Subject: [PATCH 31/35] [yolofix] Should have a list of string to be able to join() later --- src/yunohost/tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 679bf1190..28b507707 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -608,8 +608,8 @@ def tools_upgrade(operation_logger, apps=None, system=False): # randomly from yunohost itself... upgrading them is likely to critical_packages = ("moulinette", "yunohost", "yunohost-admin", "ssowat", "python") - critical_packages_upgradable = [p for p in upgradables if p["name"] in critical_packages] - noncritical_packages_upgradable = [p for p in upgradables if p["name"] not in critical_packages] + critical_packages_upgradable = [p["name"] for p in upgradables if p["name"] in critical_packages] + noncritical_packages_upgradable = [p["name"] for p in upgradables if p["name"] not in critical_packages] # Prepare dist-upgrade command dist_upgrade = "DEBIAN_FRONTEND=noninteractive" From f31c8c7475f054482852ac44d7b263fa5371d095 Mon Sep 17 00:00:00 2001 From: pitfd Date: Mon, 14 Oct 2019 09:14:40 +0000 Subject: [PATCH 32/35] Translated using Weblate (German) Currently translated at 39.4% (218 of 554 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 1fe279d6b..6699f508c 100644 --- a/locales/de.json +++ b/locales/de.json @@ -413,5 +413,6 @@ "global_settings_setting_security_password_admin_strength": "Stärke des Admin-Passworts", "global_settings_key_doesnt_exists": "Der Schlüssel'{settings_key:s}' existiert nicht in den globalen Einstellungen, du kannst alle verfügbaren Schlüssel sehen, indem du 'yunohost settings list' ausführst", "log_app_makedefault": "Mache '{}' zur Standard-Anwendung", - "hook_json_return_error": "Konnte die Rückkehr vom Einsprungpunkt {path:s} nicht lesen. Fehler: {msg:s}. Unformatierter Inhalt: {raw_content}" + "hook_json_return_error": "Konnte die Rückkehr vom Einsprungpunkt {path:s} nicht lesen. Fehler: {msg:s}. Unformatierter Inhalt: {raw_content}", + "app_full_domain_unavailable": "Es tut uns leid, aber diese Anwendung erfordert die Installation einer vollständigen Domäne, aber einige andere Anwendungen sind bereits auf der Domäne'{domain}' installiert. Eine mögliche Lösung ist das Hinzufügen und Verwenden einer Subdomain, die dieser Anwendung zugeordnet ist." } From 3aea7f6b04b26de1c634b8029fbe63da63a388ff Mon Sep 17 00:00:00 2001 From: xaloc33 Date: Thu, 10 Oct 2019 08:24:02 +0000 Subject: [PATCH 33/35] Translated using Weblate (Catalan) Currently translated at 100.0% (554 of 554 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/ca/ --- locales/ca.json | 385 ++++++++++++++++++++++++++---------------------- 1 file changed, 209 insertions(+), 176 deletions(-) diff --git a/locales/ca.json b/locales/ca.json index f5c040670..1476d5fb5 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -1,31 +1,31 @@ { "action_invalid": "Acció '{action:s}' invàlida", "admin_password": "Contrasenya d'administració", - "admin_password_change_failed": "No s'ha pogut canviar la contrasenya", + "admin_password_change_failed": "No es pot canviar la contrasenya", "admin_password_changed": "S'ha canviat la contrasenya d'administració", "app_already_installed": "{app:s} ja està instal·lada", "app_already_installed_cant_change_url": "Aquesta aplicació ja està instal·lada. La URL no és pot canviar únicament amb aquesta funció. Mireu a \"app changeurl\" si està disponible.", "app_already_up_to_date": "{app:s} ja està actualitzada", - "app_argument_choice_invalid": "Aquesta opció no és vàlida per l'argument '{name:s}', ha de ser una de {choices:s}", - "app_argument_invalid": "Valor invàlid per l'argument '{name:s}':{error:s}", + "app_argument_choice_invalid": "Utilitzeu una de les opcions «{choices:s}» per l'argument «{name:s}»", + "app_argument_invalid": "Escolliu un valor vàlid per l'argument «{name:s}»: {error:s}", "app_argument_required": "Es necessita l'argument '{name:s}'", "app_change_no_change_url_script": "L'aplicació {app_name:s} encara no permet canviar la seva URL, es possible que s'hagi d'actualitzar.", - "app_change_url_failed_nginx_reload": "No s'ha pogut tornar a carregar nginx. Aquí teniu el resultat de \"nginx -t\":\n{nginx_errors:s}", + "app_change_url_failed_nginx_reload": "No s'ha pogut tornar a carregar NGINX. Aquí teniu el resultat de \"nginx -t\":\n{nginx_errors:s}", "app_change_url_identical_domains": "L'antic i el nou domini/camí són idèntics ('{domain:s}{path:s}'), no hi ha res per fer.", - "app_change_url_no_script": "Aquesta aplicació '{app_name:s}' encara no permet modificar la URL. Potser s'ha d'actualitzar l'aplicació.", - "app_change_url_success": "La URL de {app:s} s'ha canviat correctament a {domain:s}{path:s}", + "app_change_url_no_script": "Aquesta aplicació '{app_name:s}' encara no permet modificar la URL. Potser s'ha d'actualitzar.", + "app_change_url_success": "La URL de {app:s} ara és {domain:s}{path:s}", "app_extraction_failed": "No s'han pogut extreure els fitxers d'instal·lació", - "app_id_invalid": "Id de l'aplicació incorrecte", + "app_id_invalid": "ID de l'aplicació incorrecte", "app_incompatible": "L'aplicació {app} no és compatible amb la teva versió de YunoHost", - "app_install_files_invalid": "Fitxers d'instal·lació invàlids", - "app_location_already_used": "L'aplicació '{app}' ja està instal·lada en aquest camí ({path})", + "app_install_files_invalid": "Aquests fitxers no es poden instal·lar", + "app_location_already_used": "L'aplicació «{app}» ja està instal·lada en ({path})", "app_make_default_location_already_used": "No es pot fer l'aplicació '{app}' per defecte en el domini {domain} ja que ja és utilitzat per una altra aplicació '{other_app}'", - "app_location_install_failed": "No s'ha pogut instal·lar l'aplicació en aquest camí ja que entra en conflicte amb l'aplicació '{other_app}' ja instal·lada a '{other_path}'", - "app_location_unavailable": "Aquesta url no està disponible o entra en conflicte amb aplicacions ja instal·lades:\n{apps:s}", - "app_manifest_invalid": "Manifest d'aplicació incorrecte: {error}", + "app_location_install_failed": "No s'ha pogut instal·lar l'aplicació aquí ja que entra en conflicte amb l'aplicació «{other_app}» ja instal·lada a «{other_path}»", + "app_location_unavailable": "Aquesta URL no està disponible o entra en conflicte amb aplicacions ja instal·lades:\n{apps:s}", + "app_manifest_invalid": "Hi ha algun error amb el manifest de l'aplicació: {error}", "app_no_upgrade": "No hi ha cap aplicació per actualitzar", "app_not_correctly_installed": "{app:s} sembla estar mal instal·lada", - "app_not_installed": "L'aplicació «{app:s}» no està instal·lada. Aquí hi ha la llista d'aplicacions instal·lades: {all_apps}", + "app_not_installed": "No s'ha trobat l'aplicació «{app:s}» en la llista d'aplicacions instal·lades: {all_apps}", "app_not_properly_removed": "{app:s} no s'ha pogut suprimir correctament", "app_package_need_update": "El paquet de l'aplicació {app} ha de ser actualitzat per poder seguir els canvis de YunoHost", "app_removed": "{app:s} ha estat suprimida", @@ -35,22 +35,22 @@ "app_sources_fetch_failed": "No s'han pogut carregar els fitxers font, l'URL és correcta?", "app_unknown": "Aplicació desconeguda", "app_unsupported_remote_type": "El tipus remot utilitzat per l'aplicació no està suportat", - "app_upgrade_app_name": "Actualitzant l'aplicació {app}…", + "app_upgrade_app_name": "Actualitzant {app}…", "app_upgrade_failed": "No s'ha pogut actualitzar {app:s}", "app_upgrade_some_app_failed": "No s'han pogut actualitzar algunes aplicacions", - "app_upgraded": "{app:s} ha estat actualitzada", + "app_upgraded": "S'ha actualitzat {app:s}", "appslist_corrupted_json": "No s'han pogut carregar les llistes d'aplicacions. Sembla que {filename:s} està danyat.", "appslist_could_not_migrate": "No s'ha pogut migrar la llista d'aplicacions {appslist:s}! No s'ha pogut analitzar la URL... L'antic cronjob s'ha guardat a {bkp_file:s}.", - "appslist_fetched": "S'ha descarregat la llista d'aplicacions {appslist:s} correctament", + "appslist_fetched": "S'ha actualitzat la llista d'aplicacions {appslist:s}", "appslist_migrating": "Migrant la llista d'aplicacions {appslist:s}…", "appslist_name_already_tracked": "Ja hi ha una llista d'aplicacions registrada amb el nom {name:s}.", "appslist_removed": "S'ha eliminat la llista d'aplicacions {appslist:s}", - "appslist_retrieve_bad_format": "L'arxiu obtingut per la llista d'aplicacions {appslist:s} no és vàlid", + "appslist_retrieve_bad_format": "No s'ha pogut llegir la llista d'aplicacions obtinguda {appslist:s}", "appslist_retrieve_error": "No s'ha pogut obtenir la llista d'aplicacions remota {appslist:s}: {error:s}", "appslist_unknown": "La llista d'aplicacions {appslist:s} es desconeguda.", "appslist_url_already_tracked": "Ja hi ha una llista d'aplicacions registrada amb al URL {url:s}.", "ask_current_admin_password": "Contrasenya d'administrador actual", - "ask_email": "Correu electrònic", + "ask_email": "Adreça de correu electrònic", "ask_firstname": "Nom", "ask_lastname": "Cognom", "ask_list_to_remove": "Llista per a suprimir", @@ -58,31 +58,31 @@ "ask_new_admin_password": "Nova contrasenya d'administrador", "ask_password": "Contrasenya", "ask_path": "Camí", - "backup_abstract_method": "Encara no s'ha implementat aquest mètode de copia de seguretat", + "backup_abstract_method": "Encara està per implementar aquest mètode de còpia de seguretat", "backup_action_required": "S'ha d'especificar què s'ha de guardar", "backup_app_failed": "No s'ha pogut fer la còpia de seguretat de l'aplicació \"{app:s}\"", "backup_applying_method_borg": "Enviant tots els fitxers de la còpia de seguretat al repositori borg-backup…", "backup_applying_method_copy": "Còpia de tots els fitxers a la còpia de seguretat…", "backup_applying_method_custom": "Crida del mètode de còpia de seguretat personalitzat \"{method:s}\"…", - "backup_applying_method_tar": "Creació de l'arxiu tar de la còpia de seguretat…", - "backup_archive_app_not_found": "L'aplicació \"{app:s}\" no es troba dins l'arxiu de la còpia de seguretat", + "backup_applying_method_tar": "Creació de l'arxiu TAR de la còpia de seguretat…", + "backup_archive_app_not_found": "No s'ha pogut trobar l'aplicació «{app:s}» dins l'arxiu de la còpia de seguretat", "backup_archive_broken_link": "No s'ha pogut accedir a l'arxiu de la còpia de seguretat (enllaç invàlid cap a {path:s})", "backup_archive_mount_failed": "No s'ha pogut carregar l'arxiu de la còpia de seguretat", - "backup_archive_name_exists": "Ja hi ha una còpia de seguretat amb aquest nom", + "backup_archive_name_exists": "Ja hi ha una còpia de seguretat amb aquest nom.", "backup_archive_name_unknown": "Còpia de seguretat local \"{name:s}\" desconeguda", "backup_archive_open_failed": "No s'ha pogut obrir l'arxiu de la còpia de seguretat", - "backup_archive_system_part_not_available": "La part \"{part:s}\" del sistema no està disponible en aquesta copia de seguretat", - "backup_archive_writing_error": "No es poden afegir arxius a l'arxiu comprimit de la còpia de seguretat", + "backup_archive_system_part_not_available": "La part «{part:s}» del sistema no està disponible en aquesta copia de seguretat", + "backup_archive_writing_error": "No es poden afegir els arxius «{source:s}» (anomenats en l'arxiu «{dest:s}») a l'arxiu comprimit de la còpia de seguretat «{archive:s}»", "backup_ask_for_copying_if_needed": "Alguns fitxers no s'han pogut preparar per la còpia de seguretat utilitzant el mètode que evita malgastar espai del sistema temporalment. Per fer la còpia de seguretat, s'han d'utilitzar {size:s}MB temporalment. Hi esteu d'acord?", "backup_borg_not_implemented": "El mètode de còpia de seguretat Borg encara no està implementat", - "backup_cant_mount_uncompress_archive": "No es pot carregar en mode de lectura només el directori de l'arxiu descomprimit", + "backup_cant_mount_uncompress_archive": "No es pot carregar l'arxiu descomprimit com a protegit contra escriptura", "backup_cleaning_failed": "No s'ha pogut netejar el directori temporal de la còpia de seguretat", "backup_copying_to_organize_the_archive": "Copiant {size:s}MB per organitzar l'arxiu", "backup_couldnt_bind": "No es pot lligar {src:s} amb {dest:s}.", "backup_created": "S'ha creat la còpia de seguretat", "backup_creating_archive": "Creant l'arxiu de la còpia de seguretat…", "aborting": "Avortant.", - "app_not_upgraded": "Les següents aplicacions no s'han actualitzat: {apps}", + "app_not_upgraded": "L'aplicació «{failed_app}» no s'ha pogut actualitzar, i com a conseqüència l'actualització de les següents aplicacions ha estat cancel·lada: {apps}", "app_start_install": "instal·lant l'aplicació {app}…", "app_start_remove": "Eliminant l'aplicació {app}…", "app_start_backup": "Recuperant els fitxers pels que s'ha de fer una còpia de seguretat per {app}…", @@ -90,24 +90,24 @@ "app_upgrade_several_apps": "S'actualitzaran les següents aplicacions: {apps}", "ask_new_domain": "Nou domini", "ask_new_path": "Nou camí", - "backup_actually_backuping": "S'està creant un arxiu de còpia de seguretat a partir dels fitxers recuperats…", - "backup_creation_failed": "Ha fallat la creació de la còpia de seguretat", + "backup_actually_backuping": "Creant un arxiu de còpia de seguretat a partir dels fitxers recuperats…", + "backup_creation_failed": "No s'ha pogut crear l'arxiu de la còpia de seguretat", "backup_csv_addition_failed": "No s'han pogut afegir fitxers per a fer-ne la còpia de seguretat al fitxer CSV", - "backup_csv_creation_failed": "No s'ha pogut crear el fitxer CSV necessari per a futures operacions de recuperació", - "backup_custom_backup_error": "El mètode de còpia de seguretat personalitzat ha fallat a l'etapa \"backup\"", - "backup_custom_mount_error": "El mètode de còpia de seguretat personalitzat ha fallat a l'etapa \"mount\"", + "backup_csv_creation_failed": "No s'ha pogut crear el fitxer CSV necessari per a la restauració", + "backup_custom_backup_error": "El mètode de còpia de seguretat personalitzat ha fallat a l'etapa «backup»", + "backup_custom_mount_error": "El mètode de còpia de seguretat personalitzat ha fallat a l'etapa «mount»", "backup_custom_need_mount_error": "El mètode de còpia de seguretat personalitzat ha fallat a l'etapa \"need_mount\"", - "backup_delete_error": "No s'ha pogut suprimir \"{path:s}\"", + "backup_delete_error": "No s'ha pogut suprimir «{path:s}»", "backup_deleted": "S'ha suprimit la còpia de seguretat", "backup_extracting_archive": "Extraient l'arxiu de la còpia de seguretat…", - "backup_hook_unknown": "Script de còpia de seguretat \"{hook:s}\" desconegut", - "backup_invalid_archive": "Arxiu de còpia de seguretat no vàlid", - "backup_method_borg_finished": "La còpia de seguretat a borg ha acabat", + "backup_hook_unknown": "Script de còpia de seguretat «{hook:s}» desconegut", + "backup_invalid_archive": "Aquest no és un arxiu de còpia de seguretat", + "backup_method_borg_finished": "La còpia de seguretat a Borg ha acabat", "backup_method_copy_finished": "La còpia de la còpia de seguretat ha acabat", "backup_method_custom_finished": "El mètode de còpia de seguretat personalitzat \"{method:s}\" ha acabat", - "backup_method_tar_finished": "S'ha creat l'arxiu de còpia de seguretat tar", + "backup_method_tar_finished": "S'ha creat l'arxiu de còpia de seguretat TAR", "backup_mount_archive_for_restore": "Preparant l'arxiu per la restauració…", - "good_practices_about_user_password": "Esteu a punt de definir una nova contrasenya d'usuari. La contrasenya ha de tenir un mínim de 8 caràcters ; tot i que és de bona pràctica utilitzar una contrasenya més llarga (és a dir una frase de contrasenya) i/o utilitzar diferents tipus de caràcters (majúscules, minúscules, dígits i caràcters especials).", + "good_practices_about_user_password": "Esteu a punt de definir una nova contrasenya d'usuari. La contrasenya ha de tenir un mínim de 8 caràcters; tot i que és de bona pràctica utilitzar una contrasenya més llarga (és a dir una frase de contrasenya) i/o utilitzar diferents tipus de caràcters (majúscules, minúscules, dígits i caràcters especials).", "password_listed": "Aquesta contrasenya és una de les més utilitzades en el món. Si us plau utilitzeu-ne una més única.", "password_too_simple_1": "La contrasenya ha de tenir un mínim de 8 caràcters", "password_too_simple_2": "La contrasenya ha de tenir un mínim de 8 caràcters i ha de contenir dígits, majúscules i minúscules", @@ -115,42 +115,42 @@ "password_too_simple_4": "La contrasenya ha de tenir un mínim de 12 caràcters i tenir dígits, majúscules, minúscules i caràcters especials", "backup_no_uncompress_archive_dir": "El directori de l'arxiu descomprimit no existeix", "backup_nothings_done": "No hi ha res a guardar", - "backup_output_directory_forbidden": "Directori de sortida no permès. Les còpies de seguretat no es poden crear ni dins els directoris /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ni dins els subdirectoris /home/yunohost.backup/archives", - "backup_output_directory_not_empty": "El directori de sortida no està buit", + "backup_output_directory_forbidden": "Escolliu un directori de sortida different. Les còpies de seguretat no es poden crear ni dins els directoris /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ni dins els subdirectoris /home/yunohost.backup/archives", + "backup_output_directory_not_empty": "Heu d'escollir un directori de sortida buit", "backup_output_directory_required": "Heu d'especificar un directori de sortida per la còpia de seguretat", - "backup_output_symlink_dir_broken": "Teniu un enllaç simbòlic trencat en lloc del directori dels arxius '{path:s}'. Pot ser teniu una configuració per la còpia de seguretat específica en un altre sistema de fitxers, si és el cas segurament heu oblidat muntar o connectar el disc dur o la clau USB.", - "backup_php5_to_php7_migration_may_fail": "No s'ha pogut convertir l'arxiu per suportar php7, la restauració de les vostres aplicacions pot fallar (raó: {error:s})", + "backup_output_symlink_dir_broken": "Teniu un enllaç simbòlic trencat en lloc del directori del arxiu «{path:s}». Pot ser teniu una configuració per la còpia de seguretat específica en un altre sistema de fitxers, si és el cas segurament heu oblidat muntar o connectar el disc dur o la clau USB.", + "backup_php5_to_php7_migration_may_fail": "No s'ha pogut convertir l'arxiu per suportar PHP 7, pot ser que no es puguin restaurar les vostres aplicacions PHP (raó: {error:s})", "backup_running_hooks": "Executant els scripts de la còpia de seguretat…", "backup_system_part_failed": "No s'ha pogut fer la còpia de seguretat de la part \"{part:s}\" del sistema", - "backup_unable_to_organize_files": "No s'han pogut organitzar els fitxers dins de l'arxiu amb el mètode ràpid", - "backup_with_no_backup_script_for_app": "L'aplicació {app:s} no té un script de còpia de seguretat. Serà ignorat.", - "backup_with_no_restore_script_for_app": "L'aplicació {app:s} no té un script de restauració, no podreu restaurar automàticament la còpia de seguretat d'aquesta aplicació.", - "certmanager_acme_not_configured_for_domain": "El certificat pel domini {domain:s} sembla que no està instal·lat correctament. Si us plau executeu primer cert-install per aquest domini.", - "certmanager_attempt_to_renew_nonLE_cert": "El certificat pel domini {domain:s} no ha estat emès per Let's Encrypt. No es pot renovar automàticament!", - "certmanager_attempt_to_renew_valid_cert": "El certificat pel domini {domain:s} està a punt de caducar! (Utilitzeu --force si sabeu el que esteu fent)", + "backup_unable_to_organize_files": "No s'ha pogut utilitzar el mètode ràpid per organitzar els fitxers dins de l'arxiu", + "backup_with_no_backup_script_for_app": "L'aplicació «{app:s}» no té un script de còpia de seguretat. Serà ignorat.", + "backup_with_no_restore_script_for_app": "L'aplicació «{app:s}» no té un script de restauració, no podreu restaurar automàticament la còpia de seguretat d'aquesta aplicació.", + "certmanager_acme_not_configured_for_domain": "El certificat pel domini «{domain:s}» sembla que no està instal·lat correctament. Si us plau executeu primer «cert-install» per aquest domini.", + "certmanager_attempt_to_renew_nonLE_cert": "El certificat pel domini «{domain:s}» no ha estat emès per Let's Encrypt. No es pot renovar automàticament!", + "certmanager_attempt_to_renew_valid_cert": "El certificat pel domini «{domain:s}» està a punt de caducar! (Utilitzeu --force si sabeu el que esteu fent)", "certmanager_attempt_to_replace_valid_cert": "Esteu intentant sobreescriure un certificat correcte i vàlid pel domini {domain:s}! (Utilitzeu --force per ometre)", "certmanager_cannot_read_cert": "S'ha produït un error al intentar obrir el certificat actual pel domini {domain:s} (arxiu: {file:s}), raó: {reason:s}", - "certmanager_cert_install_success": "S'ha instal·lat correctament un certificat Let's Encrypt pel domini {domain:s}!", - "certmanager_cert_install_success_selfsigned": "S'ha instal·lat correctament un certificat auto-signat pel domini {domain:s}!", - "certmanager_cert_renew_success": "S'ha renovat correctament el certificat Let's Encrypt pel domini {domain:s}!", + "certmanager_cert_install_success": "S'ha instal·lat correctament un certificat Let's Encrypt pel domini «{domain:s}»", + "certmanager_cert_install_success_selfsigned": "S'ha instal·lat correctament un certificat auto-signat pel domini «{domain:s}»", + "certmanager_cert_renew_success": "S'ha renovat correctament el certificat Let's Encrypt pel domini «{domain:s}»", "certmanager_cert_signing_failed": "No s'ha pogut firmar el nou certificat", - "certmanager_certificate_fetching_or_enabling_failed": "Sembla que l'activació del nou certificat per {domain:s} ha fallat…", - "certmanager_conflicting_nginx_file": "No s'ha pogut preparar el domini per al desafiament ACME: l'arxiu de configuració nginx {filepath:s} entra en conflicte i s'ha d'eliminar primer", + "certmanager_certificate_fetching_or_enabling_failed": "Sembla que utilitzar el nou certificat per {domain:s} ha fallat…", + "certmanager_conflicting_nginx_file": "No s'ha pogut preparar el domini per al desafiament ACME: l'arxiu de configuració NGINX {filepath:s} entra en conflicte i s'ha d'eliminar primer", "certmanager_couldnt_fetch_intermediate_cert": "S'ha exhaurit el temps d'esperar al intentar recollir el certificat intermedi des de Let's Encrypt. La instal·lació/renovació del certificat s'ha cancel·lat - torneu a intentar-ho més tard.", - "certmanager_domain_cert_not_selfsigned": "El certificat pel domini {domain:s} no és auto-signat Esteu segur de voler canviar-lo? (Utilitzeu --force per fer-ho)", - "certmanager_domain_dns_ip_differs_from_public_ip": "El registre DNS \"A\" pel domini {domain:s} és diferent a l'adreça IP d'aquest servidor. Si heu modificat recentment el registre A, si us plau espereu a que es propagui (hi ha eines per verificar la propagació disponibles a internet). (Si sabeu el que esteu fent, podeu utilitzar --no-checks per desactivar aquestes comprovacions.)", - "certmanager_domain_http_not_working": "Sembla que el domini {domain:s} no és accessible via HTTP. Si us plau verifiqueu que les configuracions DNS i nginx siguin correctes", + "certmanager_domain_cert_not_selfsigned": "El certificat pel domini {domain:s} no és auto-signat Esteu segur de voler canviar-lo? (Utilitzeu «--force» per fer-ho)", + "certmanager_domain_dns_ip_differs_from_public_ip": "El registre DNS \"A\" pel domini «{domain:s}» és diferent a l'adreça IP d'aquest servidor. Si heu modificat recentment el registre A, si us plau espereu a que es propagui (hi ha eines per verificar la propagació disponibles a internet). (Si sabeu el que esteu fent, podeu utilitzar «--no-checks» per desactivar aquestes comprovacions.)", + "certmanager_domain_http_not_working": "Sembla que el domini {domain:s} no és accessible via HTTP. Verifiqueu que les configuracions DNS i NGINX siguin correctes", "certmanager_domain_not_resolved_locally": "El domini {domain:s} no es pot resoldre dins del vostre servidor YunoHost. Això pot passar si heu modificat recentment el registre DNS. Si és així, si us plau espereu unes hores per a que es propagui. Si el problema continua, considereu afegir {domain:s} a /etc/hosts. (Si sabeu el que esteu fent, podeu utilitzar --no-checks per desactivar aquestes comprovacions.)", - "certmanager_domain_unknown": "Domini desconegut {domain:s}", - "certmanager_error_no_A_record": "No s'ha trobat cap registre DNS \"A\" per {domain:s}. Heu de fer que el vostre nom de domini apunti cap a la vostra màquina per tal de poder instal·lar un certificat Let's Encrypt! (Si sabeu el que esteu fent, podeu utilitzar --no-checks per desactivar aquestes comprovacions.)", + "certmanager_domain_unknown": "Domini desconegut «{domain:s}»", + "certmanager_error_no_A_record": "No s'ha trobat cap registre DNS «A» per «{domain:s}». Heu de fer que el vostre nom de domini apunti cap a la vostra màquina per tal de poder instal·lar un certificat Let's Encrypt. (Si sabeu el que esteu fent, podeu utilitzar «--no-checks» per desactivar aquestes comprovacions.)", "certmanager_hit_rate_limit": "S'han emès massa certificats recentment per aquest mateix conjunt de dominis {domain:s}. Si us plau torneu-ho a intentar més tard. Consulteu https://letsencrypt.org/docs/rate-limits/ per obtenir més detalls", - "certmanager_http_check_timeout": "S'ha exhaurit el temps d'espera quan el servidor ha intentat contactar amb ell mateix via HTTP utilitzant la seva adreça IP pública (domini domain:s} amb IP {ip:s}). Pot ser degut a hairpinning o a que el talla focs/router al que està connectat el servidor estan mal configurats.", + "certmanager_http_check_timeout": "S'ha exhaurit el temps d'espera quan el servidor ha intentat contactar amb ell mateix via HTTP utilitzant la seva adreça IP pública (domini «{domain:s}» amb IP «{ip:s}»). Pot ser degut a hairpinning o a que el talla focs/router al que està connectat el servidor estan mal configurats.", "certmanager_no_cert_file": "No s'ha pogut llegir l'arxiu del certificat pel domini {domain:s} (fitxer: {file:s})", "certmanager_self_ca_conf_file_not_found": "No s'ha trobat el fitxer de configuració per l'autoritat del certificat auto-signat (fitxer: {file:s})", "certmanager_unable_to_parse_self_CA_name": "No s'ha pogut analitzar el nom de l'autoritat del certificat auto-signat (fitxer: {file:s})", - "confirm_app_install_warning": "Atenció: aquesta aplicació funciona però no està ben integrada amb YunoHost. Algunes característiques com la autenticació única i la còpia de seguretat/restauració poden no estar disponibles. Voleu instal·lar-la de totes maneres? [{answers:s}] ", - "confirm_app_install_danger": "ATENCIÓ! Aquesta aplicació encara és experimental (si no és que no funciona directament) i és probable que trenqui el sistema! No hauríeu d'instal·lar-la a no ser que sapigueu el que feu. Esteu segurs de voler córrer aquest risc? [{answers:s}] ", - "confirm_app_install_thirdparty": "ATENCIÓ! La instal·lació d'aplicacions de terceres parts pot comprometre la integritat i seguretat del seu sistema. Faci-ho sota la seva responsabilitat.No hauríeu d'instal·lar-ne a no ser que sapigueu el que feu. Esteu segurs de voler córrer aquest risc? [{answers:s}] ", + "confirm_app_install_warning": "Atenció: Aquesta aplicació funciona, però no està ben integrada amb YunoHost. Algunes característiques com la autenticació única i la còpia de seguretat/restauració poden no estar disponibles. Voleu instal·lar-la de totes maneres? [{answers:s}] ", + "confirm_app_install_danger": "PERILL! Aquesta aplicació encara és experimental (si no és que no funciona directament)! No hauríeu d'instal·lar-la a no ser que sapigueu el que feu. No obtindreu CAP AJUDA si l'aplicació no funciona o trenca el sistema... Si accepteu el risc, escriviu «{answers:s}»", + "confirm_app_install_thirdparty": "PERILL! Aquesta aplicació no es part del catàleg d'aplicacions de YunoHost. La instal·lació d'aplicacions de terceres parts pot comprometre la integritat i seguretat del seu sistema. No hauríeu d'instal·lar-ne a no ser que sapigueu el que feu. No obtindreu CAP AJUDA si l'aplicació no funciona o trenca el sistema… Si accepteu el risc, escriviu «{answers:s}»", "custom_app_url_required": "Heu de especificar una URL per actualitzar la vostra aplicació personalitzada {app:s}", "custom_appslist_name_required": "Heu d'especificar un nom per la vostra llista d'aplicacions personalitzada", "diagnosis_debian_version_error": "No s'ha pogut obtenir la versió Debian: {error}", @@ -160,22 +160,22 @@ "diagnosis_monitor_system_error": "No es pot monitorar el sistema: {error}", "diagnosis_no_apps": "No hi ha cap aplicació instal·lada", "admin_password_too_long": "Trieu una contrasenya de menys de 127 caràcters", - "dpkg_is_broken": "No es pot fer això en aquest instant perquè dpkg/apt (els gestors de paquets del sistema) sembla estar mal configurat... Podeu intentar solucionar-ho connectant-vos per ssh i executant \"sudo dpkg --configure -a\".", + "dpkg_is_broken": "No es pot fer això en aquest instant perquè dpkg/APT (els gestors de paquets del sistema) sembla estar mal configurat… Podeu intentar solucionar-ho connectant-vos per SSH i executant «sudo dpkg --configure -a».", "dnsmasq_isnt_installed": "sembla que dnsmasq no està instal·lat, executeu \"apt-get remove bind9 && apt-get install dnsmasq\"", "domain_cannot_remove_main": "No es pot eliminar el domini principal. S'ha d'establir un nou domini primer", "domain_cert_gen_failed": "No s'ha pogut generar el certificat", "domain_created": "S'ha creat el domini", - "domain_creation_failed": "No s'ha pogut crear el domini", + "domain_creation_failed": "No s'ha pogut crear el domini {domain}: {error}", "domain_deleted": "S'ha eliminat el domini", - "domain_deletion_failed": "No s'ha pogut eliminar el domini", + "domain_deletion_failed": "No s'ha pogut eliminar el domini {domini}: {error}", "domain_exists": "El domini ja existeix", - "app_action_cannot_be_ran_because_required_services_down": "Aquesta aplicació necessita serveis que estan aturats. Abans de continuar, hauríeu d'intentar arrancar de nou els serveis següents (i també investigar perquè estan aturats) : {services}", + "app_action_cannot_be_ran_because_required_services_down": "Aquesta aplicació necessita serveis que estan aturats. Abans de continuar, hauríeu d'intentar arrancar de nou els serveis següents (i també investigar perquè estan aturats): {services}", "domain_dns_conf_is_just_a_recommendation": "Aquesta ordre mostra la configuració *recomanada*. En cap cas fa la configuració del DNS. És la vostra responsabilitat configurar la zona DNS en el vostre registrar en acord amb aquesta recomanació.", "domain_dyndns_already_subscribed": "Ja us heu subscrit a un domini DynDNS", "domain_dyndns_dynette_is_unreachable": "No s'ha pogut abastar la dynette YunoHost, o bé YunoHost no està connectat a internet correctament o bé el servidor dynette està caigut. Error: {error}", "domain_dyndns_invalid": "Domini no vàlid per utilitzar amb DynDNS", "domain_dyndns_root_unknown": "Domini DynDNS principal desconegut", - "domain_hostname_failed": "No s'ha pogut establir un nou nom d'amfitrió. Això podria causar problemes més tard (no és segur ... podria no passar res).", + "domain_hostname_failed": "No s'ha pogut establir un nou nom d'amfitrió. Això podria causar problemes més tard (podria no passar res).", "domain_uninstall_app_first": "Hi ha una o més aplicacions instal·lades en aquest domini. Desinstal·leu les abans d'eliminar el domini", "domain_unknown": "Domini desconegut", "domain_zone_exists": "El fitxer de zona DNS ja existeix", @@ -187,61 +187,61 @@ "dyndns_could_not_check_available": "No s'ha pogut verificar la disponibilitat de {domain:s} a {provider:s}.", "dyndns_ip_update_failed": "No s'ha pogut actualitzar l'adreça IP al DynDNS", "dyndns_ip_updated": "S'ha actualitzat l'adreça IP al DynDNS", - "dyndns_key_generating": "S'està generant la clau DNS, això pot trigar una estona…", + "dyndns_key_generating": "S'està generant la clau DNS… això pot trigar una estona.", "dyndns_key_not_found": "No s'ha trobat la clau DNS pel domini", "dyndns_no_domain_registered": "No hi ha cap domini registrat amb DynDNS", "dyndns_registered": "S'ha registrat el domini DynDNS", "dyndns_registration_failed": "No s'ha pogut registrar el domini DynDNS: {error:s}", - "dyndns_domain_not_provided": "El proveïdor {provider:s} no pot oferir el domini {domain:s}.", + "dyndns_domain_not_provided": "El proveïdor de DynDNS {provider:s} no pot oferir el domini {domain:s}.", "dyndns_unavailable": "El domini {domain:s} no està disponible.", "executing_command": "Execució de l'ordre « {command:s} »…", "executing_script": "Execució de l'script « {script:s} »…", "extracting": "Extracció en curs…", - "dyndns_cron_installed": "S'ha instal·lat la tasca cron pel DynDNS", + "dyndns_cron_installed": "S'ha creat la tasca cron pel DynDNS", "dyndns_cron_remove_failed": "No s'ha pogut eliminar la tasca cron per a DynDNS: {error}", "dyndns_cron_removed": "S'ha eliminat la tasca cron pel DynDNS", - "experimental_feature": "Atenció: aquesta funcionalitat és experimental i no es considera estable, no s'ha d'utilitzar a excepció de saber el que esteu fent.", + "experimental_feature": "Atenció: Aquesta funcionalitat és experimental i no es considera estable, no s'ha d'utilitzar a excepció de saber el que esteu fent.", "field_invalid": "Camp incorrecte « {:s} »", "file_does_not_exist": "El camí {path:s} no existeix.", - "firewall_reload_failed": "No s'ha pogut tornar a carregar el tallafoc", - "firewall_reloaded": "S'ha tornat a carregar el tallafoc", - "firewall_rules_cmd_failed": "No s'han pogut aplicar algunes regles del tallafoc. Mireu el registre per a més informació.", + "firewall_reload_failed": "No s'ha pogut tornar a carregar el tallafocs", + "firewall_reloaded": "S'ha tornat a carregar el tallafocs", + "firewall_rules_cmd_failed": "No s'han pogut aplicar algunes regles del tallafocs. Més informació en el registre.", "format_datetime_short": "%d/%m/%Y %H:%M", - "global_settings_bad_choice_for_enum": "Opció pel paràmetre {setting:s} incorrecta, s'ha rebut «{choice:s}» però les opcions disponibles són: {available_choices:s}", + "global_settings_bad_choice_for_enum": "Opció pel paràmetre {setting:s} incorrecta, s'ha rebut «{choice:s}», però les opcions disponibles són: {available_choices:s}", "global_settings_bad_type_for_setting": "El tipus del paràmetre {setting:s} és incorrecte. S'ha rebut {received_type:s}, però s'esperava {expected_type:s}", "global_settings_cant_open_settings": "No s'ha pogut obrir el fitxer de configuració, raó: {reason:s}", "global_settings_cant_serialize_settings": "No s'ha pogut serialitzar les dades de configuració, raó: {reason:s}", "global_settings_cant_write_settings": "No s'ha pogut escriure el fitxer de configuració, raó: {reason:s}", "global_settings_key_doesnt_exists": "La clau « {settings_key:s} » no existeix en la configuració global, podeu veure totes les claus disponibles executant « yunohost settings list »", - "global_settings_reset_success": "Èxit. S'ha fet una còpia de seguretat de la configuració anterior a {path:s}", + "global_settings_reset_success": "S'ha fet una còpia de seguretat de la configuració anterior a {path:s}", "global_settings_setting_example_bool": "Exemple d'opció booleana", "global_settings_setting_example_enum": "Exemple d'opció de tipus enumeració", "global_settings_setting_example_int": "Exemple d'opció de tipus enter", "global_settings_setting_example_string": "Exemple d'opció de tipus cadena", - "global_settings_setting_security_nginx_compatibility": "Solució de compromís entre compatibilitat i seguretat pel servidor web nginx. Afecta els criptògrafs (i altres aspectes relacionats amb la seguretat)", + "global_settings_setting_security_nginx_compatibility": "Solució de compromís entre compatibilitat i seguretat pel servidor web NGINX. Afecta els criptògrafs (i altres aspectes relacionats amb la seguretat)", "global_settings_setting_security_password_admin_strength": "Robustesa de la contrasenya d'administrador", "global_settings_setting_security_password_user_strength": "Robustesa de la contrasenya de l'usuari", "global_settings_setting_security_ssh_compatibility": "Solució de compromís entre compatibilitat i seguretat pel servidor SSH. Afecta els criptògrafs (i altres aspectes relacionats amb la seguretat)", - "global_settings_unknown_setting_from_settings_file": "Clau de configuració desconeguda: «{setting_key:s}», refusant-la i guardant-la a /etc/yunohost/settings-unknown.json", + "global_settings_unknown_setting_from_settings_file": "Clau de configuració desconeguda: «{setting_key:s}», refusada i guardada a /etc/yunohost/settings-unknown.json", "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Permetre la clau d'hoste DSA (obsolet) per la configuració del servei SSH", "global_settings_unknown_type": "Situació inesperada, la configuració {setting:s} sembla tenir el tipus {unknown_type:s} però no és un tipus reconegut pel sistema.", - "good_practices_about_admin_password": "Esteu a punt de definir una nova contrasenya d'administrador. La contrasenya ha de tenir un mínim de 8 caràcters ; tot i que és de bona pràctica utilitzar una contrasenya més llarga (és a dir una frase de contrasenya) i/o utilitzar diferents tipus de caràcters (majúscules, minúscules, dígits i caràcters especials).", - "hook_exec_failed": "No s'ha pogut executar l'script: {path:s}", - "hook_exec_not_terminated": "L'execució de l'script « {path:s} » no s'ha acabat correctament", - "hook_json_return_error": "No s'ha pogut llegir el retorn de l'script {path:s}. Error: {msg:s}. Contingut en brut: {raw_content}", - "hook_list_by_invalid": "Propietat per llistar les accions invàlida", + "good_practices_about_admin_password": "Esteu a punt de definir una nova contrasenya d'administrador. La contrasenya ha de tenir un mínim de 8 caràcters; tot i que és de bona pràctica utilitzar una contrasenya més llarga (és a dir una frase de contrasenya) i/o utilitzar diferents tipus de caràcters (majúscules, minúscules, dígits i caràcters especials).", + "hook_exec_failed": "No s'ha pogut executar el script: {path:s}", + "hook_exec_not_terminated": "El script no s'ha acabat correctament: {path:s}", + "hook_json_return_error": "No s'ha pogut llegir el retorn del script {path:s}. Error: {msg:s}. Contingut en brut: {raw_content}", + "hook_list_by_invalid": "Aquesta propietat no es pot utilitzar per llistar els hooks", "hook_name_unknown": "Nom de script « {name:s} » desconegut", "installation_complete": "Instal·lació completada", - "installation_failed": "Ha fallat la instal·lació", + "installation_failed": "Ha fallat alguna cosa amb la instal·lació", "invalid_url_format": "Format d'URL invàlid", "ip6tables_unavailable": "No podeu modificar les ip6tables aquí. O bé sou en un contenidor o bé el vostre nucli no és compatible amb aquesta opció", "iptables_unavailable": "No podeu modificar les iptables aquí. O bé sou en un contenidor o bé el vostre nucli no és compatible amb aquesta opció", - "log_corrupted_md_file": "El fitxer de metadades yaml associat amb els registres està malmès: « {md_file} »\nError: {error}", + "log_corrupted_md_file": "El fitxer de metadades YAML associat amb els registres està malmès: « {md_file} »\nError: {error}", "log_category_404": "La categoria de registres « {category} » no existeix", "log_link_to_log": "El registre complet d'aquesta operació: «{desc}»", "log_help_to_get_log": "Per veure el registre de l'operació « {desc} », utilitzeu l'ordre « yunohost log display {name} »", - "log_link_to_failed_log": "L'operació « {dec} » ha fallat! Per obtenir ajuda, proveïu el registre complete de l'operació clicant aquí", - "log_help_to_get_failed_log": "L'operació « {dec} » ha fallat! Per obtenir ajuda, compartiu el registre complete de l'operació utilitzant l'ordre « yunohost log display {name} --share »", + "log_link_to_failed_log": "No s'ha pogut completar l'operació « {desc} ». Per obtenir ajuda, proveïu el registre complete de l'operació clicant aquí", + "log_help_to_get_failed_log": "No s'ha pogut completar l'operació « {desc} ». Per obtenir ajuda, compartiu el registre complete de l'operació utilitzant l'ordre « yunohost log display {name} --share »", "log_does_exists": "No hi ha cap registre per l'operació amb el nom« {log} », utilitzeu « yunohost log list » per veure tots els registre d'operació disponibles", "log_operation_unit_unclosed_properly": "L'operació no s'ha tancat de forma correcta", "log_app_addaccess": "Afegir accés a « {} »", @@ -263,7 +263,7 @@ "log_domain_remove": "Elimina el domini « {} » de la configuració del sistema", "log_dyndns_subscribe": "Subscriure's a un subdomini YunoHost « {} »", "log_dyndns_update": "Actualitza la IP associada al subdomini YunoHost « {} »", - "log_letsencrypt_cert_install": "Instal·la el certificat Let's Encrypt al domini « {} »", + "log_letsencrypt_cert_install": "Instal·la un certificat Let's Encrypt al domini « {} »", "log_selfsigned_cert_install": "Instal·la el certificat autosignat al domini « {} »", "log_letsencrypt_cert_renew": "Renova el certificat Let's Encrypt de « {} »", "log_service_enable": "Activa el servei « {} »", @@ -278,79 +278,79 @@ "log_tools_upgrade": "Actualitza els paquets del sistema", "log_tools_shutdown": "Apaga el servidor", "log_tools_reboot": "Reinicia el servidor", - "already_up_to_date": "No hi ha res a fer! Tot està al dia!", + "already_up_to_date": "No hi ha res a fer. Tot està actualitzat.", "dpkg_lock_not_available": "No es pot utilitzar aquesta comanda en aquest moment ja que sembla que un altre programa està utilitzant el lock de dpkg (el gestor de paquets del sistema)", "global_settings_setting_security_postfix_compatibility": "Solució de compromís entre compatibilitat i seguretat pel servidor Postfix. Afecta els criptògrafs (i altres aspectes relacionats amb la seguretat)", "ldap_init_failed_to_create_admin": "La inicialització de LDAP no ha pogut crear l'usuari admin", "ldap_initialized": "S'ha iniciat LDAP", "license_undefined": "indefinit", - "mail_alias_remove_failed": "No s'han pogut eliminar els alias del correu «{mail:s}»", - "mail_domain_unknown": "Domini d'adreça de correu «{domain:s}» desconegut", + "mail_alias_remove_failed": "No s'han pogut eliminar els àlies del correu «{mail:s}»", + "mail_domain_unknown": "Domini d'adreça de correu per «{domain:s}» desconegut", "mail_forward_remove_failed": "No s'han pogut eliminar el reenviament de correu «{mail:s}»", - "mailbox_used_space_dovecot_down": "S'ha d'engegar el servei de correu Dovecot per poder obtenir l'espai utilitzat per la bústia de correu", - "mail_unavailable": "Aquesta adreça de correu esta reservada i ha de ser atribuïda automàticament el primer usuari", + "mailbox_used_space_dovecot_down": "S'ha d'engegar el servei de correu Dovecot, per poder obtenir l'espai utilitzat per la bústia de correu", + "mail_unavailable": "Aquesta adreça de correu està reservada i ha de ser atribuïda automàticament el primer usuari", "maindomain_change_failed": "No s'ha pogut canviar el domini principal", "maindomain_changed": "S'ha canviat el domini principal", - "migrate_tsig_end": "La migració cap a hmac-sha512 s'ha acabat", - "migrate_tsig_failed": "Ha fallat la migració del domini dyndns {domain} cap a hmac-sha512, anul·lant les modificacions. Error: {error_code} - {error}", - "migrate_tsig_start": "L'algoritme de generació de claus no es prou segur per a la signatura TSIG del domini «{domain}», començant la migració cap a un de més segur hmac-sha512", - "migrate_tsig_wait": "Esperar 3 minuts per a que el servidor dyndns tingui en compte la nova clau…", + "migrate_tsig_end": "La migració cap a HMAC-SHA-512 s'ha acabat", + "migrate_tsig_failed": "Ha fallat la migració del domini DynDNS «{domain}» cap a HMAC-SHA-512, anul·lant les modificacions. Error: {error_code}, {error}", + "migrate_tsig_start": "L'algoritme de generació de claus no es prou segur per a la signatura TSIG del domini «{domain}», començant la migració cap a un de més segur HMAC-SHA-512", + "migrate_tsig_wait": "Esperant tres minuts per a que el servidor DynDNS tingui en compte la nova clau…", "migrate_tsig_wait_2": "2 minuts…", "migrate_tsig_wait_3": "1 minut…", "migrate_tsig_wait_4": "30 segons…", - "migrate_tsig_not_needed": "Sembla que no s'utilitza cap domini dyndns, no és necessari fer cap migració!", + "migrate_tsig_not_needed": "Sembla que no s'utilitza cap domini DynDNS, no és necessari fer cap migració.", "migration_description_0001_change_cert_group_to_sslcert": "Canvia els permisos del grup dels certificats de «metronome» a «ssl-cert»", - "migration_description_0002_migrate_to_tsig_sha256": "Millora la seguretat de dyndns TSIG utilitzant SHA512 en lloc de MD5", + "migration_description_0002_migrate_to_tsig_sha256": "Millora la seguretat de DynDNS TSIG utilitzant SHA-512 en lloc de MD5", "migration_description_0003_migrate_to_stretch": "Actualització del sistema a Debian Stretch i YunoHost 3.0", "migration_description_0004_php5_to_php7_pools": "Tornar a configurar els pools PHP per utilitzar PHP 7 en lloc de PHP 5", - "migration_description_0005_postgresql_9p4_to_9p6": "Migració de les bases de dades de postgresql 9.4 a 9.6", + "migration_description_0005_postgresql_9p4_to_9p6": "Migració de les bases de dades de PostgreSQL 9.4 a 9.6", "migration_description_0006_sync_admin_and_root_passwords": "Sincronitzar les contrasenyes admin i root", "migration_description_0007_ssh_conf_managed_by_yunohost_step1": "La configuració SSH serà gestionada per YunoHost (pas 1, automàtic)", "migration_description_0008_ssh_conf_managed_by_yunohost_step2": "La configuració SSH serà gestionada per YunoHost (pas 2, manual)", "migration_description_0009_decouple_regenconf_from_services": "Desvincula el mecanisme regen-conf dels serveis", - "migration_description_0010_migrate_to_apps_json": "Elimina la appslists (desfasat) i utilitza la nova llista unificada «apps.json» en el seu lloc", + "migration_description_0010_migrate_to_apps_json": "Elimina les llistes d'aplicacions obsoletes i utilitza la nova llista unificada «apps.json» en el seu lloc", "migration_0003_backward_impossible": "La migració Stretch no és reversible.", "migration_0003_start": "Ha començat la migració a Stretch. Els registres estaran disponibles a {logfile}.", "migration_0003_patching_sources_list": "Modificant el fitxer sources.lists…", "migration_0003_main_upgrade": "Començant l'actualització principal…", - "migration_0003_fail2ban_upgrade": "Començant l'actualització de fail2ban…", + "migration_0003_fail2ban_upgrade": "Començant l'actualització de Fail2Ban…", "migration_0003_restoring_origin_nginx_conf": "El fitxer /etc/nginx/nginx.conf ha estat editat. La migració el tornarà al seu estat original... El fitxer anterior estarà disponible com a {backup_dest}.", - "migration_0003_yunohost_upgrade": "Començant l'actualització del paquet yunohost... La migració acabarà, però l'actualització actual es farà just després. Després de completar aquesta operació, pot ser que us hagueu de tornar a connectar a la web d'administració.", + "migration_0003_yunohost_upgrade": "Començant l'actualització del paquet YunoHost... La migració acabarà, però l'actualització actual es farà just després. Després de completar aquesta operació, pot ser que us hagueu de tornar a connectar a la web d'administració.", "migration_0003_not_jessie": "La distribució Debian actual no és Jessie!", "migration_0003_system_not_fully_up_to_date": "El vostre sistema no està completament actualitzat. S'ha de fer una actualització normal abans de fer la migració a Stretch.", - "migration_0003_still_on_jessie_after_main_upgrade": "Hi ha hagut un problema durant l'actualització principal: el sistema encara està amb Jessie!? Per investigar el problema, mireu el registres a {log}:s…", - "migration_0003_general_warning": "Tingueu en compte que la migració és una operació delicada. Tot i que l'equip de YunoHost a fet els possibles per revisar-la i provar-la, la migració pot provocar errors en parts del sistema o aplicacions.\n\nPer tant, recomanem:\n - Fer una còpia de seguretat de les dades o aplicacions importants. Més informació a https://yunohost.org/backup;\n - Sigueu pacient un cop llençada la migració: en funció de la connexió a internet i el maquinari, pot trigar fins a unes hores per actualitzar-ho tot.\n\nD'altra banda, el port per SMTP, utilitzat per clients de correu externs (com Thunderbird o K9-Mail) ha canviat de 465 (SSL/TLS) a 587 (STARTTLS). L'antic port 465 serà tancat automàticament i el nou port 587 serà obert en el tallafocs. Tots els usuaris *hauran* d'adaptar la configuració dels clients de correu en acord amb aquests canvis!", - "migration_0003_problematic_apps_warning": "Tingueu en compte que s'han detectat les aplicacions, possiblement, problemàtiques següents. Sembla que aquestes no s'han instal·lat des d'una applist o que no estan marcades com a «working». Per conseqüent, no podem garantir que segueixin funcionant després de l'actualització: {problematic_apps}", + "migration_0003_still_on_jessie_after_main_upgrade": "Hi ha hagut un problema durant l'actualització principal: El sistema encara està amb Jessie? Per investigar el problema, mireu el registres a {log}:s…", + "migration_0003_general_warning": "Tingueu en compte que la migració és una operació delicada. L'equip de YunoHost a fet els possibles per revisar-la i provar-la, però la migració pot provocar errors en parts del sistema o aplicacions.\n\nPer tant, es recomana:\n - Fer una còpia de seguretat de les dades o aplicacions importants. Més informació a https://yunohost.org/backup;\n - Sigueu pacient un cop llençada la migració: en funció de la connexió a internet i el maquinari, pot trigar fins a unes hores per actualitzar-ho tot.\n\nD'altra banda, el port per SMTP, utilitzat per clients de correu externs (com Thunderbird o K9-Mail) ha canviat de 465 (SSL/TLS) a 587 (STARTTLS). L'antic port (465) serà tancat automàticament, i el nou port (587) serà obert en el tallafocs. Tots els usuaris *hauran* d'adaptar la configuració dels clients de correu en acord amb aquests canvis.", + "migration_0003_problematic_apps_warning": "Tingueu en compte que s'han detectat les aplicacions, possiblement, problemàtiques següents. Sembla que aquestes no s'han instal·lat des d'una applist, o que no estan marcades com a «working». Per conseqüent, no podem garantir que segueixin funcionant després de l'actualització: {problematic_apps}", "migration_0003_modified_files": "Tingueu en compte que s'han detectat els següents fitxers que han estat modificats manualment i podrien sobreescriure's al final de l'actualització: {manually_modified_files}", - "migration_0005_postgresql_94_not_installed": "Postgresql no està instal·lat en el sistema. No hi ha res per fer!", - "migration_0005_postgresql_96_not_installed": "S'ha trobat Postgresql 9.4 instal·lat, però no Postgresql 9.6!? Alguna cosa estranya a passat en el sistema :( …", - "migration_0005_not_enough_space": "No hi ha prou espai disponible en {path} per fer la migració en aquest moment :(.", + "migration_0005_postgresql_94_not_installed": "PostgreSQL no està instal·lat en el sistema. No hi ha res per fer.", + "migration_0005_postgresql_96_not_installed": "PostgreSQL 9.4 està instal·lat, però no PostgreSQL 9.6? Alguna cosa estranya a passat en el sistema :( …", + "migration_0005_not_enough_space": "Creu espai disponible en {path} per executar la migració.", "migration_0006_disclaimer": "YunoHost esperar que les contrasenyes admin i root estiguin sincronitzades. Fent aquesta migració, la contrasenya root serà reemplaçada per la contrasenya admin.", "migration_0007_cancelled": "YunoHost no ha pogut millorar la gestió de la configuració SSH.", "migration_0007_cannot_restart": "No es pot reiniciar SSH després d'haver intentat cancel·lar la migració numero 6.", "migration_0008_general_disclaimer": "Per millorar la seguretat del servidor, es recomana que sigui YunoHost qui gestioni la configuració SSH. La configuració SSH actual és diferent a la configuració recomanada. Si deixeu que YunoHost ho reconfiguri, la manera de connectar-se al servidor mitjançant SSH canviarà de la següent manera:", - "migration_0008_port": " - la connexió es farà utilitzant el port 22 en lloc del port SSH personalitzat actual. Es pot reconfigurar;", - "migration_0008_root": " - no es podrà connectar com a root a través de SSH. S'haurà d'utilitzar l'usuari admin per fer-ho;", - "migration_0008_dsa": " - es desactivarà la clau DSA. Per tant, es podria haver d'invalidar un missatge esgarrifós del client SSH, i tornar a verificar l'empremta digital del servidor;", + "migration_0008_port": "• La connexió es farà utilitzant el port 22 en lloc del port SSH personalitzat actual. Es pot reconfigurar;", + "migration_0008_root": "• No es podrà connectar com a root a través de SSH. S'haurà d'utilitzar l'usuari admin per fer-ho;", + "migration_0008_dsa": "• Es desactivarà la clau DSA. Per tant, es podria haver d'invalidar un missatge esgarrifós del client SSH, i tornar a verificar l'empremta digital del servidor;", "migration_0008_warning": "Si heu entès els avisos i accepteu que YunoHost sobreescrigui la configuració actual, comenceu la migració. Sinó, podeu saltar-vos la migració, tot i que no està recomanat.", - "migration_0008_no_warning": "No s'han detectat riscs importants per sobreescriure la configuració SSH, però no es pot estar del tot segur ;)! Si accepteu que YunoHost sobreescrigui la configuració actual, comenceu la migració. Sinó, podeu saltar-vos la migració, tot i que no està recomanat.", - "migration_0009_not_needed": "Sembla que ja s'ha fet aquesta migració? Ometent.", + "migration_0008_no_warning": "No s'han identificat riscs importants per sobreescriure la configuració SSH, però no es pot estar del tot segur ;)! Executetu la migració per sobreescriure-la. Sinó, podeu saltar-vos la migració, tot i que no està recomanat.", + "migration_0009_not_needed": "Sembla que ja s'ha fet aquesta migració… (?) Ometent.", "migrations_backward": "Migració cap enrere.", "migrations_bad_value_for_target": "Nombre invàlid pel paràmetre target, els nombres de migració disponibles són 0 o {}", "migrations_cant_reach_migration_file": "No s'ha pogut accedir als fitxers de migració al camí %s", "migrations_current_target": "La migració objectiu és {}", "migrations_error_failed_to_load_migration": "ERROR: no s'ha pogut carregar la migració {number} {name}", "migrations_forward": "Migració endavant", - "migrations_list_conflict_pending_done": "No es pot utilitzar --previous i --done al mateix temps.", - "migrations_loading_migration": "Carregant la migració {number} {name}…", - "migrations_migration_has_failed": "La migració {number} {name} ha fallat amb l'excepció {exception}, cancel·lant", + "migrations_list_conflict_pending_done": "No es pot utilitzar «--previous» i «--done» al mateix temps.", + "migrations_loading_migration": "Carregant la migració {id}…", + "migrations_migration_has_failed": "La migració {id} ha fallat, cancel·lant. Error: {exception}", "migrations_no_migrations_to_run": "No hi ha cap migració a fer", "migrations_show_currently_running_migration": "Fent la migració {number} {name}…", "migrations_show_last_migration": "L'última migració feta és {}", - "migrations_skip_migration": "Saltant migració {number} {name}…", + "migrations_skip_migration": "Saltant migració {id}…", "migrations_success": "S'ha completat la migració {number} {name} amb èxit!", - "migrations_to_be_ran_manually": "La migració {number} {name} s'ha de fer manualment. Aneu a Eines > Migracions a la interfície admin, o executeu «yunohost tools migrations migrate».", - "migrations_need_to_accept_disclaimer": "Per fer la migració {number} {name}, heu d'acceptar aquesta clàusula de no responsabilitat:\n---\n{disclaimer}\n---\nSi accepteu fer la migració, torneu a executar l'ordre amb l'opció --accept-disclaimer.", + "migrations_to_be_ran_manually": "La migració {id} s'ha de fer manualment. Aneu a Eines → Migracions a la interfície admin, o executeu «yunohost tools migrations migrate».", + "migrations_need_to_accept_disclaimer": "Per fer la migració {id}, heu d'acceptar aquesta clàusula de no responsabilitat:\n---\n{disclaimer}\n---\nSi accepteu fer la migració, torneu a executar l'ordre amb l'opció «--accept-disclaimer».", "monitor_disabled": "El monitoratge del servidor ha estat desactivat", "monitor_enabled": "El monitoratge del servidor ha estat activat", "monitor_glances_con_failed": "No s'ha pogut connectar al servidor Glances", @@ -384,18 +384,18 @@ "pattern_firstname": "Ha de ser un nom vàlid", "pattern_lastname": "Ha de ser un cognom vàlid", "pattern_listname": "Ha d'estar compost per caràcters alfanumèrics i guió baix exclusivament", - "pattern_mailbox_quota": "Ha de ser una mida amb el sufix b/k/M/G/T o 0 per desactivar la quota", + "pattern_mailbox_quota": "Ha de ser una mida amb el sufix b/k/M/G/T o 0 per no tenir quota", "pattern_password": "Ha de tenir un mínim de 3 caràcters", "pattern_port": "Ha de ser un número de port vàlid (i.e. 0-65535)", "pattern_port_or_range": "Ha de ser un número de port vàlid (i.e. 0-65535) o un interval de ports (ex. 100:200)", "pattern_positive_number": "Ha de ser un nombre positiu", "pattern_username": "Ha d'estar compost per caràcters alfanumèrics en minúscula i guió baix exclusivament", - "pattern_password_app": "Les contrasenyes no haurien de tenir els següents caràcters: {forbidden_chars}", + "pattern_password_app": "Les contrasenyes no poden de tenir els següents caràcters: {forbidden_chars}", "port_already_closed": "El port {port:d} ja està tancat per les connexions {ip_version:s}", "port_already_opened": "El port {port:d} ja està obert per les connexions {ip_version:s}", "port_available": "El port {port:d} està disponible", "port_unavailable": "El port {port:d} no està disponible", - "recommend_to_add_first_user": "La post instal·lació s'ha acabat, però YunoHost necessita com a mínim un usuari per funcionar correctament, hauríeu d'afegir un usuari executant «yunohost user create $username» o amb la interfície d'administració.", + "recommend_to_add_first_user": "La post instal·lació s'ha acabat, però YunoHost necessita com a mínim un usuari per funcionar correctament, hauríeu d'afegir un usuari executant «yunohost user create »; o fer-ho des de la interfície d'administració.", "regenconf_file_backed_up": "S'ha guardat una còpia de seguretat del fitxer de configuració «{conf}» a «{backup}»", "regenconf_file_copy_failed": "No s'ha pogut copiar el nou fitxer de configuració «{new}» a «{conf}»", "regenconf_file_kept_back": "S'espera que el fitxer de configuració «{conf}» sigui suprimit per regen-conf (categoria {category}) però s'ha mantingut.", @@ -406,26 +406,26 @@ "regenconf_file_updated": "El fitxer de configuració «{conf}» ha estat actualitzat", "regenconf_now_managed_by_yunohost": "El fitxer de configuració «{conf}» serà gestionat per YunoHost a partir d'ara (categoria {category}).", "regenconf_up_to_date": "La configuració ja està al dia per la categoria «{category}»", - "regenconf_updated": "La configuració ha estat actualitzada per la categoria «{category}»", + "regenconf_updated": "La configuració per la categoria «{category}» ha estat actualitzada", "regenconf_would_be_updated": "La configuració hagués estat actualitzada per la categoria «{category}»", "regenconf_dry_pending_applying": "Verificació de la configuració pendent que s'hauria d'haver aplicat per la categoria «{category}»…", "regenconf_failed": "No s'ha pogut regenerar la configuració per la/les categoria/es : {categories}", "regenconf_pending_applying": "Aplicació de la configuració pendent per la categoria «{category}»…", "restore_action_required": "S'ha d'especificar quelcom a restaurar", - "restore_already_installed_app": "Ja hi ha una aplicació instal·lada amb l'id «{app:s}»", + "restore_already_installed_app": "Una aplicació amb la ID «{app:s}» ja està instal·lada", "restore_app_failed": "No s'ha pogut restaurar l'aplicació «{app:s}»", "restore_cleaning_failed": "No s'ha pogut netejar el directori temporal de restauració", "restore_complete": "Restauració completada", "restore_confirm_yunohost_installed": "Esteu segur de voler restaurar un sistema ja instal·lat? [{answers:s}]", "restore_extracting": "Extracció dels fitxers necessaris de l'arxiu…", "restore_failed": "No s'ha pogut restaurar el sistema", - "restore_hook_unavailable": "L'script de restauració «{part:s}» no està disponible en el sistema i tampoc és en l'arxiu", - "restore_may_be_not_enough_disk_space": "Sembla que no hi ha prou espai disponible en el disc (espai lliure: {free_space:d} B, espai necessari: {needed_space:d} B, marge de seguretat: {margin:d} B)", + "restore_hook_unavailable": "El script de restauració «{part:s}» no està disponible en el sistema i tampoc és en l'arxiu", + "restore_may_be_not_enough_disk_space": "Sembla que no hi ha prou espai disponible en el sistema (lliure: {free_space:d} B, espai necessari: {needed_space:d} B, marge de seguretat: {margin:d} B)", "restore_mounting_archive": "Muntatge de l'arxiu a «{path:s}»", - "restore_not_enough_disk_space": "No hi ha prou espai disponible en el disc (espai lliure: {free_space:d} B, espai necessari: {needed_space:d} B, marge de seguretat: {margin:d} B)", + "restore_not_enough_disk_space": "No hi ha prou espai disponible (espai: {free_space:d} B, espai necessari: {needed_space:d} B, marge de seguretat: {margin:d} B)", "restore_nothings_done": "No s'ha restaurat res", "restore_removing_tmp_dir_failed": "No s'ha pogut eliminar un directori temporal antic", - "restore_running_app_script": "Execució de l'script de restauració de l'aplicació «{app:s}»…", + "restore_running_app_script": "Restaurant l'aplicació «{app:s}»…", "restore_running_hooks": "Execució dels hooks de restauració…", "restore_system_part_failed": "No s'ha pogut restaurar la part «{part:s}» del sistema", "root_password_desynchronized": "S'ha canviat la contrasenya d'administració, però YunoHost no ha pogut propagar-ho cap a la contrasenya root!", @@ -439,24 +439,24 @@ "service_already_started": "Ja s'ha iniciat el servei «{service:s}»", "service_already_stopped": "Ja s'ha aturat el servei «{service:s}»", "service_cmd_exec_failed": "No s'ha pogut executar l'ordre «{command:s}»", - "service_description_avahi-daemon": "permet accedir al servidor via yunohost.local en la xarxa local", - "service_description_dnsmasq": "gestiona la resolució del nom de domini (DNS)", - "service_description_dovecot": "permet als clients de correu accedir/recuperar correus (via IMAP i POP3)", - "service_description_fail2ban": "protegeix contra els atacs de força bruta i a altres atacs provinents d'Internet", - "service_description_glances": "monitora la informació del sistema en el servidor", - "service_description_metronome": "gestiona els comptes de missatgeria instantània XMPP", - "service_description_mysql": "guarda les dades de les aplicacions (base de dades SQL)", - "service_description_nginx": "serveix o permet l'accés a totes les pàgines web allotjades en el servidor", - "service_description_nslcd": "gestiona les connexions shell dels usuaris YunoHost", - "service_description_php7.0-fpm": "executa les aplicacions escrites en PHP amb nginx", - "service_description_postfix": "utilitzat per enviar i rebre correus", - "service_description_redis-server": "una base de dades especialitzada per l'accés ràpid a dades, files d'espera i comunicació entre programes", - "service_description_rmilter": "verifica diferents paràmetres en els correus", - "service_description_rspamd": "filtra el correu brossa, i altres funcionalitats relacionades al correu", - "service_description_slapd": "guarda el usuaris, dominis i informació relacionada", - "service_description_ssh": "permet la connexió remota al servidor via terminal (protocol SSH)", - "service_description_yunohost-api": "gestiona les interaccions entre la interfície web de YunoHost i el sistema", - "service_description_yunohost-firewall": "gestiona els ports de connexió oberts i tancats als serveis", + "service_description_avahi-daemon": "Permet accedir al servidor via «yunohost.local» en la xarxa local", + "service_description_dnsmasq": "Gestiona la resolució del nom de domini (DNS)", + "service_description_dovecot": "Permet als clients de correu accedir/recuperar correus (via IMAP i POP3)", + "service_description_fail2ban": "Protegeix contra els atacs de força bruta i a altres atacs provinents d'Internet", + "service_description_glances": "Monitora la informació del sistema en el servidor", + "service_description_metronome": "Gestiona els comptes de missatgeria instantània XMPP", + "service_description_mysql": "Guarda les dades de les aplicacions (base de dades SQL)", + "service_description_nginx": "Serveix o permet l'accés a totes les pàgines web allotjades en el servidor", + "service_description_nslcd": "Gestiona les connexions shell dels usuaris YunoHost", + "service_description_php7.0-fpm": "Executa les aplicacions escrites en PHP amb NGINX", + "service_description_postfix": "Utilitzat per enviar i rebre correus", + "service_description_redis-server": "Una base de dades especialitzada per l'accés ràpid a dades, files d'espera i comunicació entre programes", + "service_description_rmilter": "Verifica diferents paràmetres en els correus", + "service_description_rspamd": "Filtra el correu brossa, i altres funcionalitats relacionades amb el correu", + "service_description_slapd": "Guarda el usuaris, dominis i informació relacionada", + "service_description_ssh": "Permet la connexió remota al servidor via terminal (protocol SSH)", + "service_description_yunohost-api": "Gestiona les interaccions entre la interfície web de YunoHost i el sistema", + "service_description_yunohost-firewall": "Gestiona els ports de connexió oberts i tancats als serveis", "service_disable_failed": "No s'han pogut deshabilitar el servei «{service:s}»\n\nRegistres recents: {logs:s}", "service_disabled": "S'ha deshabilitat el servei {service:s}", "service_enable_failed": "No s'ha pogut activar el servei «{service:s}»\n\nRegistres recents: {log:s}", @@ -479,26 +479,26 @@ "service_unknown": "Servei «{service:s}» desconegut", "ssowat_conf_generated": "S'ha generat la configuració SSOwat", "ssowat_conf_updated": "S'ha actualitzat la configuració SSOwat", - "ssowat_persistent_conf_read_error": "Error en llegir la configuració persistent de SSOwat: {error:s}. Modifiqueu el fitxer /etc/ssowat/conf.json.persistent per arreglar la sintaxi JSON", - "ssowat_persistent_conf_write_error": "Error guardant la configuració persistent de SSOwat: {error:s}. Modifiqueu el fitxer /etc/ssowat/conf.json.persistent per arreglar la sintaxi JSON", + "ssowat_persistent_conf_read_error": "No s'ha pogut llegir la configuració persistent de SSOwat: {error:s}. Modifiqueu el fitxer /etc/ssowat/conf.json.persistent per arreglar la sintaxi JSON", + "ssowat_persistent_conf_write_error": "No s'ha pogut guardar la configuració persistent de SSOwat: {error:s}. Modifiqueu el fitxer /etc/ssowat/conf.json.persistent per arreglar la sintaxi JSON", "system_upgraded": "S'ha actualitzat el sistema", - "system_username_exists": "El nom d'usuari ja existeix en els usuaris de sistema", - "this_action_broke_dpkg": "Aquesta acció a trencat dpkg/apt (els gestors de paquets del sistema)… Podeu intentar resoldre el problema connectant-vos amb SSH i executant «sudo dpkg --configure -a».", - "tools_upgrade_at_least_one": "Especifiqueu --apps O --system", + "system_username_exists": "El nom d'usuari ja existeix en la llista d'usuaris de sistema", + "this_action_broke_dpkg": "Aquesta acció a trencat dpkg/APT (els gestors de paquets del sistema)… Podeu intentar resoldre el problema connectant-vos amb SSH i executant «sudo dpkg --configure -a».", + "tools_upgrade_at_least_one": "Especifiqueu «--apps», o «--system»", "tools_upgrade_cant_both": "No es poden actualitzar tant el sistema com les aplicacions al mateix temps", "tools_upgrade_cant_hold_critical_packages": "No es poden mantenir els paquets crítics…", "tools_upgrade_cant_unhold_critical_packages": "No es poden deixar de mantenir els paquets crítics…", "tools_upgrade_regular_packages": "Actualitzant els paquets «normals» (no relacionats amb YunoHost)…", "tools_upgrade_regular_packages_failed": "No s'han pogut actualitzar els paquets següents: {packages_list}", "tools_upgrade_special_packages": "Actualitzant els paquets «especials» (relacionats amb YunoHost)…", - "tools_upgrade_special_packages_explanation": "Aquesta acció s'acabarà, però l'actualització especial continuarà en segon pla. No comenceu cap altra acció al servidor en els pròxims ~10 minuts (depèn de la velocitat del maquinari). Un cop acabat, pot ser que us hagueu de tornar a connectar a la interfície d'administració. Els registres de l'actualització estaran disponibles a Eines > Registres (a la interfície d'administració) o amb «yunohost log list» (a la línia d'ordres).", - "tools_upgrade_special_packages_completed": "Actualització dels paquets YunoHost acabada!\nPremeu [Enter] per tornar a la línia d'ordres", + "tools_upgrade_special_packages_explanation": "Aquesta acció s'acabarà, però l'actualització especial continuarà en segon pla. No comenceu cap altra acció al servidor en els pròxims ~10 minuts (depèn de la velocitat del maquinari). Un cop acabat, pot ser que us hagueu de tornar a connectar a la interfície d'administració. Els registres de l'actualització estaran disponibles a Eines → Registres (a la interfície d'administració) o amb «yunohost log list» (des de la línia d'ordres).", + "tools_upgrade_special_packages_completed": "Actualització dels paquets YunoHost acabada.\nPremeu [Enter] per tornar a la línia d'ordres", "unbackup_app": "L'aplicació «{app:s}» no serà guardada", "unexpected_error": "Hi ha hagut un error inesperat: {error}", "unit_unknown": "Unitat desconeguda «{unit:s}»", "unlimit": "Sense quota", "unrestore_app": "L'aplicació «{app:s} no serà restaurada", - "update_apt_cache_failed": "No s'ha pogut actualitzar la memòria cau d'APT (el gestor de paquets de Debian). Aquí teniu les línies de sources.list que poden ajudar-vos a identificar les línies problemàtiques:\n{sourceslist}", + "update_apt_cache_failed": "No s'ha pogut actualitzar la memòria cau d'APT (el gestor de paquets de Debian). Aquí teniu les línies de sources.list, que poden ajudar-vos a identificar les línies problemàtiques:\n{sourceslist}", "update_apt_cache_warning": "Hi ha hagut errors al actualitzar la memòria cau d'APT (el gestor de paquets de Debian). Aquí teniu les línies de sources.list que poden ajudar-vos a identificar les línies problemàtiques:\n{sourceslist}", "updating_apt_cache": "Obtenció de les actualitzacions disponibles per als paquets del sistema…", "updating_app_lists": "Obtenció de les actualitzacions disponibles per a les aplicacions…", @@ -507,21 +507,21 @@ "upnp_dev_not_found": "No s'ha trobat cap dispositiu UPnP", "upnp_disabled": "S'ha desactivat UPnP", "upnp_enabled": "S'ha activat UPnP", - "upnp_port_open_failed": "No s'han pogut obrir els ports UPnP", + "upnp_port_open_failed": "No s'ha pogut obrir el port UPnP", "user_created": "S'ha creat l'usuari", - "user_creation_failed": "No s'ha pogut crear l'usuari", + "user_creation_failed": "No s'ha pogut crear l'usuari {user}: {error}", "user_deleted": "S'ha suprimit l'usuari", - "user_deletion_failed": "No s'ha pogut suprimir l'usuari", - "user_home_creation_failed": "No s'ha pogut crear la carpeta personal («home») de l'usuari", + "user_deletion_failed": "No s'ha pogut suprimir l'usuari {user}: {error}", + "user_home_creation_failed": "No s'ha pogut crear la carpeta personal «home» per l'usuari", "user_info_failed": "No s'ha pogut obtenir la informació de l'usuari", "user_unknown": "Usuari desconegut: {user:s}", - "user_update_failed": "No s'ha pogut actualitzar l'usuari", - "user_updated": "S'ha actualitzat l'usuari", + "user_update_failed": "No s'ha pogut actualitzar l'usuari {user}: {error}", + "user_updated": "S'ha canviat la informació de l'usuari", "users_available": "Usuaris disponibles:", "yunohost_already_installed": "YunoHost ja està instal·lat", "yunohost_ca_creation_failed": "No s'ha pogut crear l'autoritat de certificació", "yunohost_ca_creation_success": "S'ha creat l'autoritat de certificació local.", - "yunohost_configured": "S'ha configurat YunoHost", + "yunohost_configured": "YunoHost està configurat", "yunohost_installing": "Instal·lació de YunoHost…", "yunohost_not_installed": "YunoHost no està instal·lat o no està instal·lat correctament. Executeu «yunohost tools postinstall»", "apps_permission_not_found": "No s'ha trobat cap permís per les aplicacions instal·lades", @@ -534,14 +534,14 @@ "group_already_disallowed": "El grup «{group:s}» ja té els permisos «{permission:s}» desactivats per l'aplicació «{app:s}»", "group_name_already_exist": "El grup {name:s} ja existeix", "group_created": "S'ha creat el grup «{group}»", - "group_creation_failed": "No s'ha pogut crear el grup «{group}»", + "group_creation_failed": "No s'ha pogut crear el grup «{group}»: {error}", "group_deleted": "S'ha eliminat el grup «{group}»", - "group_deletion_failed": "No s'ha pogut eliminar el grup «{group}»", + "group_deletion_failed": "No s'ha pogut eliminar el grup «{group}»: {error}", "group_deletion_not_allowed": "El grup {group:s} no es pot eliminar manualment.", "group_info_failed": "Ha fallat la informació del grup", "group_unknown": "Grup {group:s} desconegut", "group_updated": "S'ha actualitzat el grup «{group}»", - "group_update_failed": "No s'ha pogut actualitzat el grup «{group}»", + "group_update_failed": "No s'ha pogut actualitzat el grup «{group}»: {error}", "log_permission_add": "Afegir el permís «{}» per l'aplicació «{}»", "log_permission_remove": "Suprimir el permís «{}»", "log_permission_update": "Actualitzar el permís «{}» per l'aplicació «{}»", @@ -550,31 +550,31 @@ "log_user_group_update": "Actualitzar grup «{}»", "log_user_permission_add": "Actualitzar el permís «{}»", "log_user_permission_remove": "Actualitzar el permís «{}»", - "mailbox_disabled": "La bústia de correu està desactivada per als usuaris {user:s}", + "mailbox_disabled": "La bústia de correu està desactivada per al usuari {user:s}", "migration_description_0011_setup_group_permission": "Configurar el grup d'usuaris i els permisos per les aplicacions i els serveis", "migration_0011_backup_before_migration": "Creant una còpia de seguretat de la base de dades LDAP i la configuració de les aplicacions abans d'efectuar la migració.", "migration_0011_can_not_backup_before_migration": "No s'ha pogut fer la còpia de seguretat abans de la migració. No s'ha pogut fer la migració. Error: {error:s}", "migration_0011_create_group": "Creant un grup per a cada usuari…", "migration_0011_done": "Migració completa. Ja podeu gestionar grups d'usuaris.", - "migration_0011_LDAP_config_dirty": "Sembla que heu modificat manualment la configuració LDAP. Per fer aquesta migració s'ha d'actualitzar la configuració LDAP.\nGuardeu la configuració actual, reinicieu la configuració original amb l'ordre «yunohost tools regen-conf -f» i torneu a intentar la migració", + "migration_0011_LDAP_config_dirty": "Sembla que heu modificat manualment la configuració LDAP. Per fer aquesta migració s'ha d'actualitzar la configuració LDAP.\nGuardeu la configuració actual, reinicieu la configuració original executant l'ordre «yunohost tools regen-conf -f» i torneu a intentar la migració", "migration_0011_LDAP_update_failed": "Ha fallat l'actualització de LDAP. Error: {error:s}", "migration_0011_migrate_permission": "Fent la migració dels permisos de la configuració de les aplicacions a LDAP…", - "migration_0011_migration_failed_trying_to_rollback": "La migració ha fallat … s'intenta tornar el sistema a l'estat anterior.", + "migration_0011_migration_failed_trying_to_rollback": "La migració ha fallat… s'intenta tornar el sistema a l'estat anterior.", "migration_0011_rollback_success": "S'ha tornat el sistema a l'estat anterior.", "migration_0011_update_LDAP_database": "Actualitzant la base de dades LDAP…", "migration_0011_update_LDAP_schema": "Actualitzant l'esquema LDAP…", "need_define_permission_before": "Heu de tornar a redefinir els permisos utilitzant «yunohost user permission add -u USER» abans d'eliminar un grup permès", "permission_already_clear": "Ja s'ha donat el permís «{permission:s}» per l'aplicació {app:s}", - "permission_already_exist": "El permís «{permission:s}» ja existeix per l'aplicació {app:s}", - "permission_created": "S'ha creat el permís «{permission:s}» per l'aplicació {app:s}", - "permission_creation_failed": "La creació del permís ha fallat", - "permission_deleted": "S'ha eliminat el permís «{permission:s}» per l'aplicació {app:s}", - "permission_deletion_failed": "L'eliminació del permís «{permission:s}» per l'aplicació {app:s} ha fallat", - "permission_not_found": "No s'ha trobat el permís «{permission:s}» per l'aplicació {app:s}", + "permission_already_exist": "El permís «{permission:s}» ja existeix", + "permission_created": "S'ha creat el permís «{permission:s}»", + "permission_creation_failed": "No s'ha pogut crear el permís «{permission}»: {error}", + "permission_deleted": "S'ha eliminat el permís «{permission:s}»", + "permission_deletion_failed": "No s'ha pogut eliminar el permís «{permission:s}»: {error}", + "permission_not_found": "No s'ha trobat el permís «{permission:s}»", "permission_name_not_valid": "El nom del permís «{permission:s}» no és vàlid", - "permission_update_failed": "L'actualització del permís ha fallat", + "permission_update_failed": "No s'ha pogut actualitzar el permís «{permission}»: {error}", "permission_generated": "S'ha actualitzat la base de dades del permís", - "permission_updated": "S'ha actualitzat el permís «{permission:s}» per l'aplicació {app:s}", + "permission_updated": "S'ha actualitzat el permís «{permission:s}»", "permission_update_nothing_to_do": "No hi ha cap permís per actualitzar", "remove_main_permission_not_allowed": "No es pot eliminar el permís principal", "remove_user_of_group_not_allowed": "No es pot eliminar l'usuari {user:s} del grup {group:s}", @@ -582,5 +582,38 @@ "tools_update_failed_to_app_fetchlist": "No s'ha pogut actualitzar la llista d'aplicacions de YunoHost a causa de: {error}", "user_already_in_group": "L'usuari {user:s} ja és en el grup {group:s}", "user_not_in_group": "L'usuari {user:s} no és en el grup {group:s}", - "migration_description_0012_postgresql_password_to_md5_authentication": "Força l'autenticació postgresql a fer servir md5 per a les connexions locals" + "migration_description_0012_postgresql_password_to_md5_authentication": "Força l'autenticació PostgreSQL a fer servir MD5 per a les connexions locals", + "app_full_domain_unavailable": "Aquesta aplicació requereix un domini sencer per ser instal·lada, però ja hi ha altres aplicacions instal·lades al domini «{domain}». Una possible solució és afegir i utilitzar un subdomini dedicat a aquesta aplicació.", + "migrations_not_pending_cant_skip": "Aquestes migracions no estan pendents, així que no poden ser omeses: {ids}", + "app_action_broke_system": "Aquesta acció sembla haver trencat els següents serveis importants: {services}", + "log_permission_urls": "Actualitzar les URLs relacionades amb el permís «{}»", + "log_user_group_create": "Crear grup «{}»", + "log_user_permission_update": "Actualitzar els accessos per al permís «{}»", + "log_user_permission_reset": "Restablir el permís «{}»", + "permission_already_disallowed": "El grup «{group}» ja té el permís «{permission}» desactivat", + "migrations_already_ran": "Aquestes migracions ja s'han fet: {ids}", + "migrations_dependencies_not_satisfied": "No s'ha pogut executar la migració {id} perquè s'han d'executar primer les següents migracions: {dependencies_id}", + "migrations_failed_to_load_migration": "No s'ha pogut carregar la migració {id}: {error}", + "migrations_exclusive_options": "«--auto», «--skip», i «--force-rerun» són opcions mútuament excloents.", + "migrations_must_provide_explicit_targets": "Heu de proporcionar objectius explícits al utilitzar «--skip» o «--force-rerun»", + "migrations_no_such_migration": "No hi ha cap migració anomenada {id}", + "migrations_pending_cant_rerun": "Aquestes migracions encara estan pendents, així que no es poden tornar a executar: {ids}", + "migrations_running_forward": "Executant la migració {id}…", + "migrations_success_forward": "Migració {id} completada", + "apps_already_up_to_date": "Ja estan actualitzades totes les aplicacions", + "dyndns_provider_unreachable": "No s'ha pogut connectar amb el proveïdor Dyndns {provider}: o el vostre YunoHost no està ben connectat a Internet o el servidor dynette està caigut.", + "operation_interrupted": "S'ha interromput manualment l'operació?", + "group_already_exist": "El grup {group} ja existeix", + "group_already_exist_on_system": "El grup {group} ja existeix en els grups del sistema", + "group_cannot_be_edited": "El grup {group} no es pot editar manualment.", + "group_cannot_be_deleted": "El grup {group} no es pot eliminar manualment.", + "group_user_already_in_group": "L'usuari {user} ja està en el grup {group}", + "group_user_not_in_group": "L'usuari {user} no està en el grup {group}", + "log_permission_create": "Crear el permís «{}»", + "log_permission_delete": "Eliminar el permís «{}»", + "migration_0011_failed_to_remove_stale_object": "No s'ha pogut eliminar l'objecte obsolet {dn}: {error}", + "permission_already_allowed": "El grup «{group}» ja té el permís «{permission}» activat", + "permission_cannot_remove_main": "No es permet eliminar un permís principal", + "user_already_exists": "L'usuari {user} ja existeix", + "app_upgrade_stopped": "S'ha aturat l'actualització de totes les aplicacions per prevenir els possibles danys ja que hi ha hagut un error en l'actualització de l'anterior aplicació" } From 3bd90c63b3ce9a352c74c295ef056cd650429741 Mon Sep 17 00:00:00 2001 From: advocatux Date: Mon, 14 Oct 2019 17:43:39 +0000 Subject: [PATCH 34/35] Translated using Weblate (Spanish) Currently translated at 100.0% (556 of 556 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/es/ --- locales/es.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/es.json b/locales/es.json index 80b17673a..4f136460c 100644 --- a/locales/es.json +++ b/locales/es.json @@ -626,5 +626,7 @@ "permission_already_disallowed": "El grupo «{group}» ya tiene el permiso «{permission}» desactivado", "permission_cannot_remove_main": "No está permitido eliminar un permiso principal", "user_already_exists": "El usuario {user} ya existe", - "app_full_domain_unavailable": "Lamentablemente esta aplicación necesita un dominio completo para ser instalada pero ya hay otras aplicaciones instaladas en el dominio «{domain}». Una solución posible es añadir y usar un subdominio dedicado a esta aplicación." + "app_full_domain_unavailable": "Lamentablemente esta aplicación necesita un dominio completo para ser instalada pero ya hay otras aplicaciones instaladas en el dominio «{domain}». Una solución posible es añadir y usar un subdominio dedicado a esta aplicación.", + "app_install_failed": "No se pudo instalar {app}", + "app_install_script_failed": "Ha ocurrido un error en el guión de instalación de la aplicación" } From 57af2c7c45a3c621a7e52359e5e8fb11a1e608f8 Mon Sep 17 00:00:00 2001 From: xaloc33 Date: Mon, 14 Oct 2019 20:51:25 +0000 Subject: [PATCH 35/35] Translated using Weblate (Catalan) Currently translated at 100.0% (556 of 556 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/ca/ --- locales/ca.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/ca.json b/locales/ca.json index 1476d5fb5..592bc4592 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -615,5 +615,7 @@ "permission_already_allowed": "El grup «{group}» ja té el permís «{permission}» activat", "permission_cannot_remove_main": "No es permet eliminar un permís principal", "user_already_exists": "L'usuari {user} ja existeix", - "app_upgrade_stopped": "S'ha aturat l'actualització de totes les aplicacions per prevenir els possibles danys ja que hi ha hagut un error en l'actualització de l'anterior aplicació" + "app_upgrade_stopped": "S'ha aturat l'actualització de totes les aplicacions per prevenir els possibles danys ja que hi ha hagut un error en l'actualització de l'anterior aplicació", + "app_install_failed": "No s'ha pogut instal·lar {app}", + "app_install_script_failed": "Hi ha hagut un error en el script d'instal·lació de l'aplicació" }