From fbaddd900283cb57cd4da504e704278d31e84ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Tille?= Date: Sun, 25 Nov 2018 22:53:31 +0100 Subject: [PATCH] Implement permission management --- data/other/ldap_scheme.yml | 8 +- locales/en.json | 3 + src/yunohost/permission.py | 490 +++++++++++++++++++++++++++++++++++++ src/yunohost/user.py | 8 +- 4 files changed, 501 insertions(+), 8 deletions(-) create mode 100644 src/yunohost/permission.py diff --git a/data/other/ldap_scheme.yml b/data/other/ldap_scheme.yml index d30c4915c..11504bbe8 100644 --- a/data/other/ldap_scheme.yml +++ b/data/other/ldap_scheme.yml @@ -51,8 +51,8 @@ children: objectClass: - posixGroup - top - cn=ALL,ou=groups: - cn: ALL + cn=all_users,ou=groups: + cn: all_users gidNumber: "4002" objectClass: - posixGroup @@ -66,7 +66,7 @@ depends_children: - posixGroup - permissionYnh groupPermission: - - "cn=ALL,ou=groups,dc=yunohost,dc=org" + - "cn=all_users,ou=groups,dc=yunohost,dc=org" cn=main.metronome,ou=permission: cn: main.metronome gidNumber: "5002" @@ -74,4 +74,4 @@ depends_children: - posixGroup - permissionYnh groupPermission: - - "cn=ALL,ou=groups,dc=yunohost,dc=org" + - "cn=all_users,ou=groups,dc=yunohost,dc=org" diff --git a/locales/en.json b/locales/en.json index 5b78cdcd7..51cb01b0b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -271,6 +271,7 @@ "log_user_group_update": "Update '{}' group", "log_user_update": "Update information of '{}' user", "log_user_permission_add": "Update '{}' permission", + "log_user_permission_remove": "Update '{}' permisson", "log_tools_maindomain": "Make '{}' as main domain", "log_tools_migrations_migrate_forward": "Migrate forward", "log_tools_migrations_migrate_backward": "Migrate backward", @@ -357,6 +358,7 @@ "mysql_db_creation_failed": "MySQL database creation failed", "mysql_db_init_failed": "MySQL database init failed", "mysql_db_initialized": "The MySQL database has been initialized", + "need_define_permission_before": "You need to redefine the permission by 'yunohost user permission add -u USER' before to remove an allowed group", "network_check_mx_ko": "DNS MX record is not set", "network_check_smtp_ko": "Outbound mail (SMTP port 25) seems to be blocked by your network", "network_check_smtp_ok": "Outbound mail (SMTP port 25) is not blocked", @@ -400,6 +402,7 @@ "permission_name_not_valid": "Permission name '{permission:s}' not valid", "permission_update_failed": "Permission update failed", "permission_updated": "Permission '{permission:s}' for app {app:s} updated", + "permission_update_nothing_to_do": "Permission update nothing to do", "port_already_closed": "Port {port:d} is already closed for {ip_version:s} connections", "port_already_opened": "Port {port:d} is already opened for {ip_version:s} connections", "port_available": "Port {port:d} is available", diff --git a/src/yunohost/permission.py b/src/yunohost/permission.py new file mode 100644 index 000000000..df53e9b6b --- /dev/null +++ b/src/yunohost/permission.py @@ -0,0 +1,490 @@ +# -*- coding: utf-8 -*- + +""" License + + Copyright (C) 2014 YUNOHOST.ORG + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses + +""" + +""" yunohost_permission.py + + Manage permissions +""" + +import errno +import grp +import random + +from moulinette import m18n +from moulinette.core import MoulinetteError +from moulinette.utils.log import getActionLogger +from yunohost.user import user_list, user_group_list +from yunohost.app import app_ssowatconf +from yunohost.log import is_unit_operation + +logger = getActionLogger('yunohost.user') + +def user_permission_list(auth, app=None, permission=None, username=None, group=None): + """ + List permission for specific application + + Keyword argument: + app -- an application OR sftp, xmpp (metronome), mail + permission -- name of the permission ("main" by default) + username -- Username to get informations + group -- Groupname to get informations + + """ + + user_l = user_list(auth, ['uid'])['users'] + + permission_attrs = [ + 'cn', + 'groupPermission', + 'inheritPermission', + 'URL', + ] + + # Normally app is alway defined but it should be possible to set it + if app and not isinstance(app, list): + app = [app] + if permission and not isinstance(permission, list): + permission = [permission] + if not isinstance(group, list): + group = [group] + if isinstance(username, list): + group.extend(username) + else: + group.append(username) + group = filter(None, group) + + permissions = {} + + result = auth.search('ou=permission,dc=yunohost,dc=org', + '(objectclass=permissionYnh)', permission_attrs) + + for res in result: + permission_name = res['cn'][0].split('.')[0] + try: + app_name = res['cn'][0].split('.')[1] + except: + logger.warning(m18n.n('permission_name_not_valid', permission=per)) + group_name = [] + if 'groupPermission' in res: + for g in res['groupPermission']: + group_name.append(g.split("=")[1].split(",")[0]) + user_name = [] + if 'inheritPermission' in res: + for u in res['inheritPermission']: + user_name.append(u.split("=")[1].split(",")[0]) + + # Don't show the result if the user diffined a specific permission, user or group + if app and not app_name in app: + continue + if permission and not permission_name in permission: + continue + if group and not set(group) & set(group_name): + continue + + if not app_name in permissions: + permissions[app_name] = {} + + permissions[app_name][permission_name] = {'allowed_users':[], 'allowed_groups':[]} + for g in group_name: + permissions[app_name][permission_name]['allowed_groups'].append(g) + for u in user_name: + permissions[app_name][permission_name]['allowed_users'].append(u) + if 'URL' in res: + permissions[app_name][permission_name]['URL'] = [] + for u in res['URL']: + permissions[app_name][permission_name]['URL'].append(u) + + return {'permissions': permissions} + + +def user_permission_update(operation_logger, auth, app=[], permission=None, add_username=None, add_group=None, del_username=None, del_group=None): + """ + Allow or Disallow a user or group to a permission for a specific application + + Keyword argument: + app -- an application OR sftp, xmpp (metronome), mail + permission -- name of the permission ("main" by default) + add_username -- Username to allow + add_group -- Groupname to allow + del_username -- Username to disallow + del_group -- Groupname to disallow + + """ + from yunohost.hook import hook_callback + from yunohost.user import user_group_list + + if permission: + if not isinstance(permission, list): + permission = [permission] + else: + permission = ["main"] + + if add_group: + if not isinstance(add_group, list): + add_group = [add_group] + else: + add_group = [] + + if add_username: + if not isinstance(add_username, list): + add_username = [add_username] + else: + add_username = [] + + if del_group: + if not isinstance(del_group, list): + del_group = [del_group] + else: + del_group = [] + + if del_username: + if not isinstance(del_username, list): + del_username = [del_username] + else: + del_username = [] + + # Validate that the group exist + for g in add_group: + if not g in user_group_list(auth, ['cn'])['groups']: + raise MoulinetteError(errno.EINVAL, m18n.n('group_unknown', group=g)) + for u in add_username: + if not u in user_list(auth, ['uid'])['users']: + raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=u)) + for g in del_group: + if not g in user_group_list(auth, ['cn'])['groups']: + raise MoulinetteError(errno.EINVAL, m18n.n('group_unknown', group=g)) + for u in del_username: + if not u in user_list(auth, ['uid'])['users']: + raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=u)) + + # Merge user and group (note that we consider all user as a group) + add_group.extend(add_username) + del_group.extend(del_username) + + if 'all_users' in add_group or 'all_users' in del_group: + raise MoulinetteError(errno.EINVAL, m18n.n('edit_permission_with_group_all_users_not_allowed')) + + # Populate permission informations + permission_attrs = [ + 'cn', + 'groupPermission', + ] + result = auth.search('ou=permission,dc=yunohost,dc=org', + '(objectclass=permissionYnh)', permission_attrs) + result = {p['cn'][0]: p for p in result} + + new_per_dict = {} + + for a in app: + for per in permission: + permission_name = per + '.' + a + if not permission_name in result: + raise MoulinetteError(errno.EINVAL, m18n.n('permission_not_found', permission=per, app=a)) + new_per_dict[permission_name] = set() + if 'groupPermission' in result[permission_name]: + new_per_dict[permission_name] = set(result[permission_name]['groupPermission']) + + for g in del_group: + if 'cn=all_users,ou=groups,dc=yunohost,dc=org' in new_per_dict[permission_name]: + raise MoulinetteError(errno.EINVAL, m18n.n('need_define_permission_before')) + group_name = 'cn=' + g + ',ou=groups,dc=yunohost,dc=org' + if not group_name in new_per_dict[permission_name]: + logger.warning(m18n.n('group_alread_disallowed', permission=per, app=a, group=g)) + else: + new_per_dict[permission_name].remove(group_name) + + if 'cn=all_users,ou=groups,dc=yunohost,dc=org' in new_per_dict[permission_name]: + new_per_dict[permission_name].remove('cn=all_users,ou=groups,dc=yunohost,dc=org') + for g in add_group: + group_name = 'cn=' + g + ',ou=groups,dc=yunohost,dc=org' + if group_name in new_per_dict[permission_name]: + logger.warning(m18n.n('group_alread_allowed', permission=per, app=a, group=g)) + else: + new_per_dict[permission_name].add(group_name) + + operation_logger.start() + + for per, val in new_per_dict.items(): + # Don't update LDAP if we update exactly the same values + if val == set(result[per]['groupPermission'] if 'groupPermission' in result[per] else []): + continue + if auth.update('cn=%s,ou=permission' % per, {'groupPermission': val}): + p = per.split('.') + logger.success(m18n.n('permission_updated', permission=p[0], app=p[1])) + else: + raise MoulinetteError(169, m18n.n('permission_update_failed')) + + _permission_sync_to_user(auth) + + for a in app: + allowed_users = set() + disallowed_users = set() + group_list = user_group_list(auth, ['member'])['groups'] + + for g in add_group: + if 'members' in group_list[g]: + allowed_users.union(group_list[g]['members']) + for g in del_group: + if 'members' in group_list[g]: + disallowed_users.union(group_list[g]['members']) + + allowed_users = ','.join(allowed_users) + disallowed_users = ','.join(disallowed_users) + if add_group: + hook_callback('post_app_addaccess', args=[app, allowed_users]) + if del_group: + hook_callback('post_app_removeaccess', args=[app, disallowed_users]) + + app_ssowatconf(auth) + return user_permission_list(auth, app, permission) + + +def user_permission_clear(operation_logger, auth, app=[], permission=None): + """ + Reset the permission for a specific application + + Keyword argument: + app -- an application OR sftp, xmpp (metronome), mail + permission -- name of the permission ("main" by default) + username -- Username to get informations (all by default) + group -- Groupname to get informations (all by default) + + """ + from yunohost.hook import hook_callback + + if permission: + if not isinstance(permission, list): + permission = [permission] + else: + permission = ["main"] + + default_permission = {'groupPermission': ['cn=all_users,ou=groups,dc=yunohost,dc=org']} + + # Populate permission informations + permission_attrs = [ + 'cn', + 'groupPermission', + ] + result = auth.search('ou=permission,dc=yunohost,dc=org', + '(objectclass=permissionYnh)', permission_attrs) + result = {p['cn'][0]: p for p in result} + + for a in app: + for per in permission: + permission_name = per + '.' + a + if not permission_name in result: + raise MoulinetteError(errno.EINVAL, m18n.n('permission_not_found', permission=per, app=a)) + if 'groupPermission' in result[permission_name] and 'cn=all_users,ou=groups,dc=yunohost,dc=org' in result[permission_name]['groupPermission']: + logger.warning(m18n.n('permission_already_clear', permission=per, app=a)) + continue + if auth.update('cn=%s,ou=permission' % permission_name, default_permission): + logger.success(m18n.n('permission_updated', permission=per, app=a)) + else: + raise MoulinetteError(169, m18n.n('permission_update_failed')) + + _permission_sync_to_user(auth) + + for a in app: + permission_name = 'main.' + a + result = auth.search('ou=permission,dc=yunohost,dc=org', + filter='cn=' + permission_name, attrs=['inheritPermission']) + if result: + allowed_users = result[0]['inheritPermission'] + new_user_list = ','.join(allowed_users) + hook_callback('post_app_removeaccess', args=[app, new_user_list]) + + app_ssowatconf(auth) + return user_permission_list(auth, app, permission) + + +@is_unit_operation(['permission','app']) +def permission_add(operation_logger, auth, app, permission, url=None): + """ + Create a new permission for a specific application + + Keyword argument: + app -- an application OR sftp, xmpp (metronome), mail + permission -- name of the permission ("main" by default) + url -- list of url to specify for the permission + + """ + from yunohost.domain import _normalize_domain_path + + # Validate uniqueness of permission in LDAP + permission_name = str(permission + '.' + app) # str(...) Fix encoding issue + conflict = auth.get_conflict({ + 'cn': permission_name + }, base_dn='ou=permission,dc=yunohost,dc=org') + if conflict: + raise MoulinetteError(errno.EEXIST, m18n.n('permission_already_exist', permission=permission, app=app)) + + # Get random GID + all_gid = {x.gr_gid for x in grp.getgrall()} + + uid_guid_found = False + while not uid_guid_found: + gid = str(random.randint(200, 99999)) + uid_guid_found = gid not in all_gid + + attr_dict = { + 'objectClass': ['top', 'permissionYnh', 'posixGroup'], + 'cn': permission_name, + 'gidNumber': gid, + 'groupPermission': 'cn=all_users,ou=groups,dc=yunohost,dc=org' + } + + if url: + attr_dict['URL'] = [] + for u in url: + domain = u[:u.index('/')] + path = u[u.index('/'):] + domain, path = _normalize_domain_path(domain, path) + attr_dict['URL'].append(domain + path) + + operation_logger.start() + if auth.add('cn=%s,ou=permission' % permission_name, attr_dict): + _permission_sync_to_user(auth) + logger.success(m18n.n('permission_created', permission=permission, app=app)) + return user_permission_list(auth, app, permission) + + raise MoulinetteError(169, m18n.n('premission_creation_failled')) + + +@is_unit_operation(['permission','app']) +def permission_update(operation_logger, auth, app, permission, add_url=None, remove_url=None): + """ + Update a permission for a specific application + + Keyword argument: + app -- an application OR sftp, xmpp (metronome), mail + permission -- name of the permission ("main" by default) + add_url -- Add a new url for a permission + remove_url -- Remove a url for a permission + + """ + from yunohost.domain import _normalize_domain_path + + permission_name = str(permission + '.' + app) # str(...) Fix encoding issue + + # Populate permission informations + result = auth.search(base='ou=permission,dc=yunohost,dc=org', + filter='cn=' + permission_name, attrs=['URL']) + if not result: + raise MoulinetteError(errno.EINVAL, m18n.n('permission_not_found', permission=permission, app=app)) + permission_obj = result[0] + + if not 'URL' in permission_obj: + permission_obj['URL'] = [] + + url = set(permission_obj['URL']) + + if add_url: + for u in add_url: + domain = u[:u.index('/')] + path = u[u.index('/'):] + domain, path = _normalize_domain_path(domain, path) + url.add(domain + path) + if remove_url: + for u in remove_url: + domain = u[:u.index('/')] + path = u[u.index('/'):] + domain, path = _normalize_domain_path(domain, path) + url.discard(domain + path) + + if url == set(permission_obj['URL']): + logger.warning(m18n.n('permission_update_nothing_to_do')) + return user_permission_list(auth, app, permission) + + operation_logger.start() + if auth.update('cn=%s,ou=permission' % permission_name, {'cn':permission_name, 'URL': url}): + _permission_sync_to_user(auth) + logger.success(m18n.n('permission_updated', permission=permission, app=app)) + return user_permission_list(auth, app, permission) + + raise MoulinetteError(169, m18n.n('premission_update_failled')) + + +@is_unit_operation(['permission','app']) +def permission_remove(operation_logger, auth, app, permission, force=False): + """ + Remove a permission for a specific application + + Keyword argument: + app -- an application OR sftp, xmpp (metronome), mail + permission -- name of the permission ("main" by default) + + """ + + if permission == "main" and not force: + raise MoulinetteError(errno.EPERM, m18n.n('remove_main_permission_not_allowed')) + + operation_logger.start() + if not auth.remove('cn=%s,ou=permission' % str(permission + '.' + app)): + raise MoulinetteError(169, m18n.n('permission_deletion_failed', permission=permission, app=app)) + _permission_sync_to_user(auth) + logger.success(m18n.n('permission_deleted', permission=permission, app=app)) + + +def _permission_sync_to_user(auth): + """ + Sychronise the inheritPermission attribut in the permission object from the user<->group link and the group<->permission link + """ + import os + + permission_attrs = [ + 'cn', + 'member', + 'permission', + ] + group_info = auth.search('ou=groups,dc=yunohost,dc=org', + '(objectclass=groupOfNamesYnh)', permission_attrs) + user_permission={} + + for group in group_info: + if 'permission' not in group: + continue + if not 'member' in group: + continue + for permission in group['permission']: + permission = permission.split("=")[1].split(",")[0] + if not permission in user_permission: + user_permission[permission] = set() + for member in group['member']: + user_permission[permission].add(member) + + for per in auth.search('ou=permission,dc=yunohost,dc=org', + '(objectclass=permissionYnh)', ['cn', 'inheritPermission']): + if per['cn'][0] in user_permission: + val = set(user_permission[per['cn'][0]]) + else: + # If the new value and the old value à empty nothing to do + if not 'inheritPermission' in per: + continue + val = set() + if 'inheritPermission' in per and val == set(per['inheritPermission']): + continue + uid_val = [v.split("=")[1].split(",")[0] for v in val] + inheritPermission = {'inheritPermission': val, 'memberUid': uid_val} + if not auth.update('cn=%s,ou=permission' % per['cn'][0], inheritPermission): + raise MoulinetteError(169, m18n.n('permission_update_failed')) + + # Reload unscd because if not the group is not updated in the system from LDAP + os.system('systemctl restart unscd') diff --git a/src/yunohost/user.py b/src/yunohost/user.py index b4790926b..a2ff5b5a9 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -214,10 +214,10 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas app_ssowatconf(auth) # TODO: Send a welcome mail to user logger.success(m18n.n('user_created')) - # Create group for user and add to group 'ALL' + # Create group for user and add to group 'all_users' user_group_add(auth, groupname=username, gid=uid) user_group_update(auth, groupname=username, add_user=username, force=True) - user_group_update(auth, 'ALL', add_user=username, force=True) + user_group_update(auth, 'all_users', add_user=username, force=True) hook_callback('post_user_create', args=[username, mail, password, firstname, lastname]) @@ -598,7 +598,7 @@ def user_group_delete(operation_logger, auth, groupname, force=False): from yunohost.app import app_ssowatconf from yunohost.permission import _permission_sync_to_user - if not force and (groupname == 'ALL' or groupname == 'admins' or groupname in user_list(auth, ['uid'])['users']): + if not force and (groupname == 'all_users' or groupname == 'admins' or groupname in user_list(auth, ['uid'])['users']): raise MoulinetteError(errno.EPERM, m18n.n('group_deletion_not_allowed', user=groupname)) operation_logger.start() @@ -627,7 +627,7 @@ def user_group_update(operation_logger, auth, groupname, add_user=None, remove_u attrs_to_fetch = ['member'] - if (groupname == 'ALL' or groupname == 'admins') and not force: + if (groupname == 'all_users' or groupname == 'admins') and not force: raise MoulinetteError(errno.EINVAL, m18n.n('edit_group_not_allowed', group=groupname)) # Populate group informations