Implement permission management

This commit is contained in:
Josué Tille 2018-11-25 22:53:31 +01:00
parent bb892bb1a4
commit fbaddd9002
No known key found for this signature in database
GPG key ID: D5E068C6DFA8681D
4 changed files with 501 additions and 8 deletions

View file

@ -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"

View file

@ -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",

490
src/yunohost/permission.py Normal file
View file

@ -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')

View file

@ -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