def tools_shell(command=None): """ Launch an (i)python shell in the YunoHost context. This is entirely aim for development. """ from yunohost.utils.ldap import _get_ldap_interface ldap = _get_ldap_interface() if command: exec(command) return logger.warn( "The \033[1;34mldap\033[0m interface is available in this context") try: from IPython import embed embed() except ImportError: logger.warn( "You don't have IPython installed, consider installing it as it is way better than the standard shell." ) logger.warn("Falling back on the standard shell.") import readline # will allow Up/Down/History in the console readline # to please pyflakes import code vars = globals().copy() vars.update(locals()) shell = code.InteractiveConsole(vars) shell.interact()
def user_group_info(groupname): """ Get user informations Keyword argument: groupname -- Groupname to get informations """ from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract ldap = _get_ldap_interface() # Fetch info for this group result = ldap.search('ou=groups,dc=yunohost,dc=org', "cn=" + groupname, ["cn", "member", "permission"]) if not result: raise YunohostError('group_unknown', group=groupname) infos = result[0] # Format data return { 'members': [_ldap_path_extract(p, "uid") for p in infos.get("member", [])], 'permissions': [_ldap_path_extract(p, "cn") for p in infos.get("permission", [])] }
def _get_user_for_ssh(username, attrs=None): def ssh_root_login_status(): # XXX temporary placed here for when the ssh_root commands are integrated # extracted from https://github.com/YunoHost/yunohost/pull/345 # XXX should we support all the options? # this is the content of "man sshd_config" # PermitRootLogin # Specifies whether root can log in using ssh(1). The argument must be # “yes”, “without-password”, “forced-commands-only”, or “no”. The # default is “yes”. sshd_config_content = read_file(SSHD_CONFIG_PATH) if re.search( "^ *PermitRootLogin +(no|forced-commands-only) *$", sshd_config_content, re.MULTILINE, ): return {"PermitRootLogin": False} return {"PermitRootLogin": True} if username == "root": root_unix = pwd.getpwnam("root") return { "username": "******", "fullname": "", "mail": "", "ssh_allowed": ssh_root_login_status()["PermitRootLogin"], "shell": root_unix.pw_shell, "home_path": root_unix.pw_dir, } if username == "admin": admin_unix = pwd.getpwnam("admin") return { "username": "******", "fullname": "", "mail": "", "ssh_allowed": admin_unix.pw_shell.strip() != "/bin/false", "shell": admin_unix.pw_shell, "home_path": admin_unix.pw_dir, } # TODO escape input using https://www.python-ldap.org/doc/html/ldap-filter.html from yunohost.utils.ldap import _get_ldap_interface ldap = _get_ldap_interface() user = ldap.search( "ou=users,dc=yunohost,dc=org", "(&(objectclass=person)(uid=%s))" % username, attrs, ) assert len(user) in (0, 1) if not user: return None return user[0]
def add_new_ldap_attributes(self): from yunohost.utils.ldap import _get_ldap_interface from yunohost.regenconf import regen_conf, BACKUP_CONF_DIR # Check if the migration can be processed 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( m18n.n( "migration_0019_slapd_config_will_be_overwritten", conf_backup_folder=BACKUP_CONF_DIR, )) # Update LDAP schema restart slapd logger.info(m18n.n("migration_0011_update_LDAP_schema")) regen_conf(names=["slapd"], force=True) logger.info(m18n.n("migration_0019_add_new_attributes_in_ldap")) ldap = _get_ldap_interface() permission_list = user_permission_list(full=True)["permissions"] for permission in permission_list: system_perms = { "mail": "E-mail", "xmpp": "XMPP", "ssh": "SSH", "sftp": "STFP", } if permission.split(".")[0] in system_perms: update = { "authHeader": ["FALSE"], "label": [system_perms[permission.split(".")[0]]], "showTile": ["FALSE"], "isProtected": ["TRUE"], } else: app, subperm_name = permission.split(".") if permission.endswith(".main"): update = { "authHeader": ["TRUE"], "label": [ app ], # Note that this is later re-changed during the call to migrate_legacy_permission_settings() if a 'label' setting exists "showTile": ["TRUE"], "isProtected": ["FALSE"], } else: update = { "authHeader": ["TRUE"], "label": [subperm_name.title()], "showTile": ["FALSE"], "isProtected": ["TRUE"], } ldap.update("cn=%s,ou=permission" % permission, update)
def permission_sync_to_user(): """ Sychronise the inheritPermission attribut in the permission object from the user<->group link and the group<->permission link """ import os from yunohost.app import app_ssowatconf from yunohost.user import user_group_list from yunohost.utils.ldap import _get_ldap_interface ldap = _get_ldap_interface() groups = user_group_list(full=True)["groups"] permissions = user_permission_list(full=True)["permissions"] for permission_name, permission_infos in permissions.items(): # These are the users currently allowed because there's an 'inheritPermission' object corresponding to it currently_allowed_users = set(permission_infos["corresponding_users"]) # These are the users that should be allowed because they are member of a group that is allowed for this permission ... should_be_allowed_users = set([ user for group in permission_infos["allowed"] for user in groups[group]["members"] ]) # Note that a LDAP operation with the same value that is in LDAP crash SLAP. # So we need to check before each ldap operation that we really change something in LDAP if currently_allowed_users == should_be_allowed_users: # We're all good, this permission is already correctly synchronized ! continue new_inherited_perms = { "inheritPermission": [ "uid=%s,ou=users,dc=yunohost,dc=org" % u for u in should_be_allowed_users ], "memberUid": should_be_allowed_users, } # Commit the change with the new inherited stuff try: ldap.update("cn=%s,ou=permission" % permission_name, new_inherited_perms) except Exception as e: raise YunohostError("permission_update_failed", permission=permission_name, error=e) logger.debug("The permission database has been resynchronized") app_ssowatconf() # Reload unscd, otherwise the group ain't propagated to the LDAP database os.system("nscd --invalidate=passwd") os.system("nscd --invalidate=group")
def run(self, *args): from yunohost.utils.ldap import _get_ldap_interface ldap = _get_ldap_interface() existing_perms_raw = ldap.search( "ou=permission,dc=yunohost,dc=org", "(objectclass=permissionYnh)", ["cn"] ) existing_perms = [perm["cn"][0] for perm in existing_perms_raw] # Add SSH and SFTP permissions ldap_map = read_yaml( "/usr/share/yunohost/yunohost-config/moulinette/ldap_scheme.yml" ) if "sftp.main" not in existing_perms: ldap.add( "cn=sftp.main,ou=permission", ldap_map["depends_children"]["cn=sftp.main,ou=permission"], ) if "ssh.main" not in existing_perms: ldap.add( "cn=ssh.main,ou=permission", ldap_map["depends_children"]["cn=ssh.main,ou=permission"], ) # Add a bash terminal to each users users = ldap.search( "ou=users,dc=yunohost,dc=org", filter="(loginShell=*)", attrs=["dn", "uid", "loginShell"], ) for user in users: if user["loginShell"][0] == "/bin/false": dn = user["dn"][0].replace(",dc=yunohost,dc=org", "") ldap.update(dn, {"loginShell": ["/bin/bash"]}) else: user_permission_update( "ssh.main", add=user["uid"][0], sync_perm=False ) permission_sync_to_user() # Somehow this is needed otherwise the PAM thing doesn't forget about the # old loginShell value ? subprocess.call(["nscd", "-i", "passwd"]) if ( "/etc/ssh/sshd_config" in manually_modified_files() and os.system( "grep -q '^ *AllowGroups\\|^ *AllowUsers' /etc/ssh/sshd_config" ) != 0 ): logger.error(m18n.n("diagnosis_sshd_config_insecure"))
def tools_adminpw(new_password, check_strength=True): """ Change admin password Keyword argument: new_password """ from yunohost.user import _hash_user_password from yunohost.utils.password import assert_password_is_strong_enough import spwd if check_strength: assert_password_is_strong_enough("admin", new_password) # UNIX seems to not like password longer than 127 chars ... # e.g. SSH login gets broken (or even 'su admin' when entering the password) if len(new_password) >= 127: raise YunohostValidationError("admin_password_too_long") new_hash = _hash_user_password(new_password) from yunohost.utils.ldap import _get_ldap_interface ldap = _get_ldap_interface() try: ldap.update( "cn=admin", { "userPassword": [new_hash], }, ) except Exception: logger.error("unable to change admin password") raise YunohostError("admin_password_change_failed") else: # Write as root password try: hash_root = spwd.getspnam("root").sp_pwd with open("/etc/shadow", "r") as before_file: before = before_file.read() with open("/etc/shadow", "w") as after_file: after_file.write( before.replace("root:" + hash_root, "root:" + new_hash.replace("{CRYPT}", ""))) # An IOError may be thrown if for some reason we can't read/write /etc/passwd # A KeyError could also be thrown if 'root' is not in /etc/passwd in the first place (for example because no password defined ?) # (c.f. the line about getspnam) except (IOError, KeyError): logger.warning(m18n.n("root_password_desynchronized")) return logger.info(m18n.n("root_password_replaced_by_admin_password")) logger.success(m18n.n("admin_password_changed"))
def migrate_LDAP_db(): logger.info(m18n.n("migration_0011_update_LDAP_database")) from yunohost.utils.ldap import _get_ldap_interface ldap = _get_ldap_interface() ldap_map = read_yaml( '/usr/share/yunohost/yunohost-config/moulinette/ldap_scheme.yml') try: SetupGroupPermissions.remove_if_exists("ou=permission") SetupGroupPermissions.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) 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: raise YunohostError("migration_0011_LDAP_update_failed", error=e) logger.info(m18n.n("migration_0011_create_group")) # Create a group for each yunohost user user_list = ldap.search( 'ou=users,dc=yunohost,dc=org', '(&(objectclass=person)(!(uid=root))(!(uid=nobody)))', ['uid', 'uidNumber']) for user_info in user_list: username = user_info['uid'][0] ldap.update( 'uid=%s,ou=users' % username, { 'objectClass': [ 'mailAccount', 'inetOrgPerson', 'posixAccount', 'userPermissionYnh' ] }) user_group_create(username, gid=user_info['uidNumber'][0], primary_group=True, sync_perm=False) user_group_update(groupname='all_users', add=username, force=True, sync_perm=False)
def user_group_list(short=False, full=False, include_primary_groups=True): """ List users Keyword argument: short -- Only list the name of the groups without any additional info full -- List all the info available for each groups include_primary_groups -- Include groups corresponding to users (which should always only contains this user) This option is set to false by default in the action map because we don't want to have these displayed when the user runs `yunohost user group list`, but internally we do want to list them when called from other functions """ # Fetch relevant informations from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract ldap = _get_ldap_interface() groups_infos = ldap.search( "ou=groups,dc=yunohost,dc=org", "(objectclass=groupOfNamesYnh)", ["cn", "member", "permission"], ) # Parse / organize information to be outputed users = user_list()["users"] groups = {} for infos in groups_infos: name = infos["cn"][0] if not include_primary_groups and name in users: continue groups[name] = {} groups[name]["members"] = [ _ldap_path_extract(p, "uid") for p in infos.get("member", []) ] if full: groups[name]["permissions"] = [ _ldap_path_extract(p, "cn") for p in infos.get("permission", []) ] if short: groups = list(groups.keys()) return {"groups": groups}
def user_delete(operation_logger, username, purge=False): """ Delete user Keyword argument: username -- Username to delete purge """ from yunohost.hook import hook_callback from yunohost.utils.ldap import _get_ldap_interface if username not in user_list()["users"]: raise YunohostError('user_unknown', user=username) operation_logger.start() user_group_update("all_users", remove=username, force=True, sync_perm=False) for group, infos in user_group_list()["groups"].items(): if group == "all_users": continue # If the user is in this group (and it's not the primary group), # remove the member from the group if username != group and username in infos["members"]: user_group_update(group, remove=username, sync_perm=False) # 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: ldap.remove('uid=%s,ou=users' % username) except Exception as e: raise YunohostError('user_deletion_failed', user=username, error=e) # Invalidate passwd to take user deletion into account subprocess.call(['nscd', '-i', 'passwd']) if purge: subprocess.call(['rm', '-rf', '/home/{0}'.format(username)]) subprocess.call(['rm', '-rf', '/var/mail/{0}'.format(username)]) hook_callback('post_user_delete', args=[username, purge]) logger.success(m18n.n('user_deleted'))
def user_list(fields=None): from yunohost.utils.ldap import _get_ldap_interface user_attrs = { "uid": "username", "cn": "fullname", "mail": "mail", "maildrop": "mail-forward", "loginShell": "shell", "homeDirectory": "home_path", "mailuserquota": "mailbox-quota", } attrs = ["uid"] users = {} if fields: keys = user_attrs.keys() for attr in fields: if attr in keys: attrs.append(attr) else: raise YunohostError("field_invalid", attr) else: attrs = ["uid", "cn", "mail", "mailuserquota", "loginShell"] ldap = _get_ldap_interface() result = ldap.search( "ou=users,dc=yunohost,dc=org", "(&(objectclass=person)(!(uid=root))(!(uid=nobody)))", attrs, ) for user in result: entry = {} for attr, values in user.items(): if values: if attr == "loginShell": if values[0].strip() == "/bin/false": entry["ssh_allowed"] = False else: entry["ssh_allowed"] = True entry[user_attrs[attr]] = values[0] uid = entry[user_attrs["uid"]] users[uid] = entry return {"users": users}
def user_list(fields=None): from yunohost.utils.ldap import _get_ldap_interface user_attrs = { 'uid': 'username', 'cn': 'fullname', 'mail': 'mail', 'maildrop': 'mail-forward', 'loginShell': 'shell', 'homeDirectory': 'home_path', 'mailuserquota': 'mailbox-quota' } attrs = ['uid'] users = {} if fields: keys = user_attrs.keys() for attr in fields: if attr in keys: attrs.append(attr) else: raise YunohostError('field_invalid', attr) else: attrs = ['uid', 'cn', 'mail', 'mailuserquota', 'loginShell'] ldap = _get_ldap_interface() result = ldap.search( 'ou=users,dc=yunohost,dc=org', '(&(objectclass=person)(!(uid=root))(!(uid=nobody)))', attrs) for user in result: entry = {} for attr, values in user.items(): if values: if attr == "loginShell": if values[0].strip() == "/bin/false": entry["ssh_allowed"] = False else: entry["ssh_allowed"] = True entry[user_attrs[attr]] = values[0] uid = entry[user_attrs['uid']] users[uid] = entry return {'users': users}
def user_ssh_disallow(username): """ Disallow YunoHost user connect as ssh. Keyword argument: username -- User username """ # TODO it would be good to support different kind of shells if not _get_user_for_ssh(username): raise YunohostError('user_unknown', user=username) from yunohost.utils.ldap import _get_ldap_interface ldap = _get_ldap_interface() ldap.update('uid=%s,ou=users' % username, {'loginShell': ['/bin/false']}) # Somehow this is needed otherwise the PAM thing doesn't forget about the # old loginShell value ? subprocess.call(['nscd', '-i', 'passwd'])
def user_group_delete(operation_logger, groupname, force=False, sync_perm=True): """ Delete user Keyword argument: groupname -- Groupname to delete """ from yunohost.permission import permission_sync_to_user from yunohost.utils.ldap import _get_ldap_interface existing_groups = list(user_group_list()["groups"].keys()) if groupname not in existing_groups: raise YunohostValidationError("group_unknown", group=groupname) # Refuse to delete primary groups of a user (e.g. group 'sam' related to user 'sam') # without the force option... # # We also can't delete "all_users" because that's a special group... existing_users = list(user_list()["users"].keys()) undeletable_groups = existing_users + ["all_users", "visitors"] if groupname in undeletable_groups and not force: raise YunohostValidationError("group_cannot_be_deleted", group=groupname) operation_logger.start() ldap = _get_ldap_interface() try: ldap.remove("cn=%s,ou=groups" % groupname) except Exception as e: raise YunohostError("group_deletion_failed", group=groupname, error=e) if sync_perm: permission_sync_to_user() if groupname not in existing_users: logger.success(m18n.n("group_deleted", group=groupname)) else: logger.debug(m18n.n("group_deleted", group=groupname))
def user_ssh_disallow(username): """ Disallow YunoHost user connect as ssh. Keyword argument: username -- User username """ # TODO it would be good to support different kind of shells if not _get_user_for_ssh(username): raise YunohostValidationError("user_unknown", user=username) from yunohost.utils.ldap import _get_ldap_interface ldap = _get_ldap_interface() ldap.update("uid=%s,ou=users" % username, {"loginShell": ["/bin/false"]}) # Somehow this is needed otherwise the PAM thing doesn't forget about the # old loginShell value ? subprocess.call(["nscd", "-i", "passwd"])
def permission_delete(operation_logger, permission, force=False, sync_perm=True): """ Delete a permission Keyword argument: permission -- Name of the permission (e.g. mail or nextcloud or wordpress.editors) """ # By default, manipulate main permission if "." not in permission: permission = permission + ".main" if permission.endswith(".main") and not force: raise YunohostValidationError("permission_cannot_remove_main") from yunohost.utils.ldap import _get_ldap_interface ldap = _get_ldap_interface() # Make sure this permission exists _ = user_permission_info(permission) # Actually delete the permission operation_logger.related_to.append(("app", permission.split(".")[0])) operation_logger.start() try: ldap.remove("cn=%s,ou=permission" % permission) except Exception as e: raise YunohostError("permission_deletion_failed", permission=permission, error=e) if sync_perm: permission_sync_to_user() logger.debug(m18n.n("permission_deleted", permission=permission))
def domain_list(exclude_subdomains=False): """ List domains Keyword argument: exclude_subdomains -- Filter out domains that are subdomains of other declared domains """ from yunohost.utils.ldap import _get_ldap_interface ldap = _get_ldap_interface() result = [ entry["virtualdomain"][0] for entry in ldap.search("ou=domains,dc=yunohost,dc=org", "virtualdomain=*", ["virtualdomain"]) ] result_list = [] for domain in result: if exclude_subdomains: parent_domain = domain.split(".", 1)[1] if parent_domain in result: continue result_list.append(domain) def cmp_domain(domain): # Keep the main part of the domain and the extension together # eg: this.is.an.example.com -> ['example.com', 'an', 'is', 'this'] domain = domain.split(".") domain[-1] = domain[-2] + domain.pop() domain = list(reversed(domain)) return domain result_list = sorted(result_list, key=cmp_domain) return {"domains": result_list, "main": _get_maindomain()}
def remove_if_exists(target): from yunohost.utils.ldap import _get_ldap_interface ldap = _get_ldap_interface() try: objects = ldap.search(target + ",dc=yunohost,dc=org") # ldap search will raise an exception if no corresponding object is found >.> ... except Exception: logger.debug("%s does not exist, no need to delete it" % target) return objects.reverse() for o in objects: for dn in o["dn"]: dn = dn.replace(",dc=yunohost,dc=org", "") logger.debug("Deleting old object %s ..." % dn) try: ldap.remove(dn) except Exception as e: raise YunohostError( "migration_0011_failed_to_remove_stale_object", dn=dn, error=e)
def permission_url( operation_logger, permission, url=None, add_url=None, remove_url=None, auth_header=None, clear_urls=False, 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) url -- (optional) URL for which access will be allowed/forbidden. add_url -- (optional) List of additional url to add for which access will be allowed/forbidden remove_url -- (optional) List of additional url to remove for which access will be allowed/forbidden auth_header -- (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application clear_urls -- (optional) Clean all urls (url and additional_urls) """ from yunohost.app import app_setting from yunohost.utils.ldap import _get_ldap_interface ldap = _get_ldap_interface() # By default, manipulate main permission if "." not in permission: permission = permission + ".main" app = permission.split(".")[0] if url or add_url: domain = app_setting(app, "domain") path = app_setting(app, "path") if domain is None or path is None: raise YunohostError("unknown_main_domain_path", app=app) else: app_main_path = domain + path # Fetch existing permission existing_permission = user_permission_info(permission) show_tile = existing_permission["show_tile"] if url is None: url = existing_permission["url"] else: url = _validate_and_sanitize_permission_url(url, app_main_path, app) if url.startswith("re:") and existing_permission["show_tile"]: logger.warning( m18n.n("regex_incompatible_with_tile", regex=url, permission=permission)) show_tile = False current_additional_urls = existing_permission["additional_urls"] new_additional_urls = copy.copy(current_additional_urls) if add_url: for ur in add_url: if ur in current_additional_urls: logger.warning( m18n.n("additional_urls_already_added", permission=permission, url=ur)) else: ur = _validate_and_sanitize_permission_url( ur, app_main_path, app) new_additional_urls += [ur] if remove_url: for ur in remove_url: if ur not in current_additional_urls: logger.warning( m18n.n("additional_urls_already_removed", permission=permission, url=ur)) new_additional_urls = [ u for u in new_additional_urls if u not in remove_url ] if auth_header is None: auth_header = existing_permission["auth_header"] if clear_urls: url = None new_additional_urls = [] show_tile = False # Guarantee uniqueness of all values, which would otherwise make ldap.update angry. new_additional_urls = set(new_additional_urls) # Actually commit the change operation_logger.related_to.append(("app", permission.split(".")[0])) operation_logger.start() try: ldap.update( "cn=%s,ou=permission" % permission, { "URL": [url] if url is not None else [], "additionalUrls": new_additional_urls, "authHeader": [str(auth_header).upper()], "showTile": [str(show_tile).upper()], }, ) except Exception as e: raise YunohostError("permission_update_failed", permission=permission, error=e) if sync_perm: permission_sync_to_user() logger.debug(m18n.n("permission_updated", permission=permission)) return user_permission_info(permission)
def user_permission_list(short=False, full=False, ignore_system_perms=False, absolute_urls=False): """ List permissions and corresponding accesses """ # Fetch relevant informations from yunohost.app import app_setting, _installed_apps from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract ldap = _get_ldap_interface() permissions_infos = ldap.search( "ou=permission,dc=yunohost,dc=org", "(objectclass=permissionYnh)", [ "cn", "groupPermission", "inheritPermission", "URL", "additionalUrls", "authHeader", "label", "showTile", "isProtected", ], ) # Parse / organize information to be outputed apps = sorted(_installed_apps()) apps_base_path = { app: app_setting(app, "domain") + app_setting(app, "path") for app in apps if app_setting(app, "domain") and app_setting(app, "path") } permissions = {} for infos in permissions_infos: name = infos["cn"][0] if ignore_system_perms and name.split(".")[0] in SYSTEM_PERMS: continue app = name.split(".")[0] perm = {} perm["allowed"] = [ _ldap_path_extract(p, "cn") for p in infos.get("groupPermission", []) ] if full: perm["corresponding_users"] = [ _ldap_path_extract(p, "uid") for p in infos.get("inheritPermission", []) ] perm["auth_header"] = infos.get("authHeader", [False])[0] == "TRUE" perm["label"] = infos.get("label", [None])[0] perm["show_tile"] = infos.get("showTile", [False])[0] == "TRUE" perm["protected"] = infos.get("isProtected", [False])[0] == "TRUE" perm["url"] = infos.get("URL", [None])[0] perm["additional_urls"] = infos.get("additionalUrls", []) if absolute_urls: app_base_path = ( apps_base_path[app] if app in apps_base_path else "" ) # Meh in some situation where the app is currently installed/removed, this function may be called and we still need to act as if the corresponding permission indeed exists ... dunno if that's really the right way to proceed but okay. perm["url"] = _get_absolute_url(perm["url"], app_base_path) perm["additional_urls"] = [ _get_absolute_url(url, app_base_path) for url in perm["additional_urls"] ] permissions[name] = perm # Make sure labels for sub-permissions are the form " Applabel (Sublabel) " if full: subpermissions = { k: v for k, v in permissions.items() if not k.endswith(".main") } for name, infos in subpermissions.items(): main_perm_name = name.split(".")[0] + ".main" if main_perm_name not in permissions: logger.debug( "Uhoh, unknown permission %s ? (Maybe we're in the process or deleting the perm for this app...)" % main_perm_name) continue main_perm_label = permissions[main_perm_name]["label"] infos["sublabel"] = infos["label"] infos["label"] = "%s (%s)" % (main_perm_label, infos["label"]) if short: permissions = list(permissions.keys()) return {"permissions": permissions}
def permission_create( operation_logger, permission, allowed=None, url=None, additional_urls=None, auth_header=True, label=None, show_tile=False, protected=False, 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) allowed -- (optional) List of group/user to allow for the permission url -- (optional) URL for which access will be allowed/forbidden additional_urls -- (optional) List of additional URL for which access will be allowed/forbidden auth_header -- (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application label -- (optional) Define a name for the permission. This label will be shown on the SSO and in the admin. Default is "permission name" show_tile -- (optional) Define if a tile will be shown in the SSO protected -- (optional) Define if the permission can be added/removed to the visitor group 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 '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]*$ """ from yunohost.utils.ldap import _get_ldap_interface from yunohost.user import user_group_list ldap = _get_ldap_interface() # By default, manipulate main permission if "." not in permission: permission = permission + ".main" # Validate uniqueness of permission in LDAP if ldap.get_conflict({"cn": permission}, base_dn="ou=permission,dc=yunohost,dc=org"): raise YunohostValidationError("permission_already_exist", permission=permission) # 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 app, subperm = permission.split(".") attr_dict = { "objectClass": ["top", "permissionYnh", "posixGroup"], "cn": str(permission), "gidNumber": gid, "authHeader": ["TRUE"], "label": [ str(label) if label else (subperm if subperm != "main" else app.title()) ], "showTile": [ "FALSE" ], # Dummy value, it will be fixed when we call '_update_ldap_group_permission' "isProtected": [ "FALSE" ], # Dummy value, it will be fixed when we call '_update_ldap_group_permission' } if allowed is not None: if not isinstance(allowed, list): allowed = [allowed] # Validate that the groups to add actually exist all_existing_groups = user_group_list()["groups"].keys() for group in allowed or []: if group not in all_existing_groups: raise YunohostValidationError("group_unknown", group=group) operation_logger.related_to.append(("app", permission.split(".")[0])) operation_logger.start() try: ldap.add("cn=%s,ou=permission" % permission, attr_dict) except Exception as e: raise YunohostError("permission_creation_failed", permission=permission, error=e) permission_url( permission, url=url, add_url=additional_urls, auth_header=auth_header, sync_perm=False, ) new_permission = _update_ldap_group_permission( permission=permission, allowed=allowed, label=label, show_tile=show_tile, protected=protected, sync_perm=sync_perm, ) logger.debug(m18n.n("permission_created", permission=permission)) return new_permission
def user_create(operation_logger, username, firstname, lastname, domain, password, mailbox_quota="0", mail=None): from yunohost.domain import domain_list, _get_maindomain from yunohost.hook import hook_callback from yunohost.utils.password import assert_password_is_strong_enough from yunohost.utils.ldap import _get_ldap_interface # Ensure sufficiently complex password assert_password_is_strong_enough("user", password) if mail is not None: logger.warning( "Packagers ! Using --mail in 'yunohost user create' is deprecated ... please use --domain instead." ) domain = mail.split("@")[-1] # Validate domain used for email address/xmpp account if domain is None: if msettings.get('interface') == 'api': raise YunohostError('Invalide usage, specify domain argument') else: # On affiche les differents domaines possibles msignals.display(m18n.n('domains_available')) for domain in domain_list()['domains']: msignals.display("- {}".format(domain)) maindomain = _get_maindomain() domain = msignals.prompt( m18n.n('ask_user_domain') + ' (default: %s)' % maindomain) if not domain: domain = maindomain # Check that the domain exists if domain not in domain_list()['domains']: raise YunohostError('domain_name_unknown', domain=domain) mail = username + '@' + domain ldap = _get_ldap_interface() if username in user_list()["users"]: raise YunohostError("user_already_exists", user=username) # Validate uniqueness of username and mail in LDAP try: ldap.validate_uniqueness({ 'uid': username, 'mail': mail, 'cn': username }) except Exception as e: raise YunohostError('user_creation_failed', user=username, error=e) # Validate uniqueness of username in system users all_existing_usernames = {x.pw_name for x in pwd.getpwall()} if username in all_existing_usernames: raise YunohostError('system_username_exists') main_domain = _get_maindomain() aliases = [ 'root@' + main_domain, 'admin@' + main_domain, 'webmaster@' + main_domain, 'postmaster@' + main_domain, 'abuse@' + main_domain, ] if mail in aliases: raise YunohostError('mail_unavailable') operation_logger.start() # Get random UID/GID all_uid = {str(x.pw_uid) for x in pwd.getpwall()} all_gid = {str(x.gr_gid) for x in grp.getgrall()} uid_guid_found = False while not uid_guid_found: # LXC uid number is limited to 65536 by default uid = str(random.randint(200, 65000)) uid_guid_found = uid not in all_uid and uid not in all_gid # Adapt values for LDAP fullname = '%s %s' % (firstname, lastname) attr_dict = { 'objectClass': ['mailAccount', 'inetOrgPerson', 'posixAccount', 'userPermissionYnh'], 'givenName': [firstname], 'sn': [lastname], 'displayName': [fullname], 'cn': [fullname], 'uid': [username], 'mail': mail, # NOTE: this one seems to be already a list 'maildrop': [username], 'mailuserquota': [mailbox_quota], 'userPassword': [_hash_user_password(password)], 'gidNumber': [uid], 'uidNumber': [uid], 'homeDirectory': ['/home/' + username], 'loginShell': ['/bin/false'] } # If it is the first user, add some aliases if not ldap.search(base='ou=users,dc=yunohost,dc=org', filter='uid=*'): attr_dict['mail'] = [attr_dict['mail']] + aliases try: ldap.add('uid=%s,ou=users' % username, attr_dict) except Exception as e: raise YunohostError('user_creation_failed', user=username, error=e) # Invalidate passwd and group to take user and group creation into account subprocess.call(['nscd', '-i', 'passwd']) subprocess.call(['nscd', '-i', 'group']) try: # Attempt to create user home folder subprocess.check_call(["mkhomedir_helper", username]) except subprocess.CalledProcessError: if not os.path.isdir('/home/{0}'.format(username)): logger.warning(m18n.n('user_home_creation_failed'), exc_info=1) # Create group for user and add to group 'all_users' user_group_create(groupname=username, gid=uid, primary_group=True, sync_perm=False) user_group_update(groupname='all_users', add=username, force=True, sync_perm=True) # Trigger post_user_create hooks env_dict = { "YNH_USER_USERNAME": username, "YNH_USER_MAIL": mail, "YNH_USER_PASSWORD": password, "YNH_USER_FIRSTNAME": firstname, "YNH_USER_LASTNAME": lastname } hook_callback('post_user_create', args=[username, mail], env=env_dict) # TODO: Send a welcome mail to user logger.success(m18n.n('user_created')) return {'fullname': fullname, 'username': username, 'mail': mail}
def tools_ldapinit(): """ YunoHost LDAP initialization """ with open("/usr/share/yunohost/yunohost-config/moulinette/ldap_scheme.yml" ) as f: ldap_map = yaml.load(f) from yunohost.utils.ldap import _get_ldap_interface ldap = _get_ldap_interface() for rdn, attr_dict in ldap_map["parents"].items(): try: ldap.add(rdn, attr_dict) except Exception as e: logger.warn( "Error when trying to inject '%s' -> '%s' into ldap: %s" % (rdn, attr_dict, e)) for rdn, attr_dict in ldap_map["children"].items(): try: ldap.add(rdn, attr_dict) except Exception as e: logger.warn( "Error when trying to inject '%s' -> '%s' into ldap: %s" % (rdn, attr_dict, e)) for rdn, attr_dict in ldap_map["depends_children"].items(): try: ldap.add(rdn, attr_dict) except Exception as e: logger.warn( "Error when trying to inject '%s' -> '%s' into ldap: %s" % (rdn, attr_dict, e)) admin_dict = { "cn": ["admin"], "uid": ["admin"], "description": ["LDAP Administrator"], "gidNumber": ["1007"], "uidNumber": ["1007"], "homeDirectory": ["/home/admin"], "loginShell": ["/bin/bash"], "objectClass": ["organizationalRole", "posixAccount", "simpleSecurityObject"], "userPassword": ["yunohost"], } ldap.update("cn=admin", admin_dict) # Force nscd to refresh cache to take admin creation into account subprocess.call(["nscd", "-i", "passwd"]) # Check admin actually exists now try: pwd.getpwnam("admin") except KeyError: logger.error(m18n.n("ldap_init_failed_to_create_admin")) raise YunohostError("installation_failed") try: # Attempt to create user home folder subprocess.check_call(["mkhomedir_helper", "admin"]) except subprocess.CalledProcessError: if not os.path.isdir("/home/{0}".format("admin")): logger.warning(m18n.n("user_home_creation_failed"), exc_info=1) logger.success(m18n.n("ldap_initialized"))
def user_group_update(operation_logger, groupname, add=None, remove=None, force=False, sync_perm=True): """ Update user informations Keyword argument: groupname -- Groupname to update add -- User(s) to add in group remove -- User(s) to remove in group """ from yunohost.permission import permission_sync_to_user from yunohost.utils.ldap import _get_ldap_interface existing_users = list(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... if not force: if groupname == "all_users": raise YunohostError('group_cannot_edit_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) # 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"] new_group = copy.copy(current_group) if add: users_to_add = [add] if not isinstance(add, list) else add for user in users_to_add: if user not in existing_users: raise YunohostError('user_unknown', user=user) if user in current_group: logger.warning( m18n.n('group_user_already_in_group', user=user, group=groupname)) else: operation_logger.related_to.append(('user', user)) new_group += users_to_add if remove: users_to_remove = [remove] if not isinstance(remove, list) else remove for user in users_to_remove: if user not in current_group: logger.warning( m18n.n('group_user_not_in_group', user=user, group=groupname)) else: operation_logger.related_to.append(('user', user)) # Remove users_to_remove from new_group # Kinda like a new_group -= users_to_remove new_group = [u for u in new_group if u not in users_to_remove] new_group_dns = [ "uid=" + user + ",ou=users,dc=yunohost,dc=org" for user in new_group ] if set(new_group) != set(current_group): operation_logger.start() ldap = _get_ldap_interface() try: ldap.update('cn=%s,ou=groups' % groupname, { "member": set(new_group_dns), "memberUid": set(new_group) }) except Exception as e: raise YunohostError('group_update_failed', group=groupname, error=e) if groupname != "all_users": logger.success(m18n.n('group_updated', group=groupname)) else: logger.debug(m18n.n('group_updated', group=groupname)) if sync_perm: permission_sync_to_user() return user_group_info(groupname)
def user_group_create(operation_logger, groupname, gid=None, primary_group=False, sync_perm=True): """ Create group Keyword argument: groupname -- Must be unique """ from yunohost.permission import permission_sync_to_user from yunohost.utils.ldap import _get_ldap_interface ldap = _get_ldap_interface() # Validate uniqueness of groupname in LDAP conflict = ldap.get_conflict({'cn': groupname}, base_dn='ou=groups,dc=yunohost,dc=org') if conflict: raise YunohostError('group_already_exist', group=groupname) # Validate uniqueness of groupname in system group all_existing_groupnames = {x.gr_name for x in grp.getgrall()} if groupname in all_existing_groupnames: if primary_group: logger.warning( m18n.n('group_already_exist_on_system_but_removing_it', group=groupname)) subprocess.check_call("sed --in-place '/^%s:/d' /etc/group" % groupname, shell=True) else: raise YunohostError('group_already_exist_on_system', group=groupname) if not gid: # 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', 'groupOfNamesYnh', 'posixGroup'], 'cn': groupname, 'gidNumber': gid, } # Here we handle the creation of a primary group # We want to initialize this group to contain the corresponding user # (then we won't be able to add/remove any user in this group) if primary_group: attr_dict["member"] = [ "uid=" + groupname + ",ou=users,dc=yunohost,dc=org" ] operation_logger.start() try: ldap.add('cn=%s,ou=groups' % groupname, attr_dict) except Exception as e: raise YunohostError('group_creation_failed', group=groupname, error=e) if sync_perm: permission_sync_to_user() if not primary_group: logger.success(m18n.n('group_created', group=groupname)) else: logger.debug(m18n.n('group_created', group=groupname)) return {'name': groupname}
def user_info(username): """ Get user informations Keyword argument: username -- Username or mail to get informations """ from yunohost.utils.ldap import _get_ldap_interface ldap = _get_ldap_interface() user_attrs = [ 'cn', 'mail', 'uid', 'maildrop', 'givenName', 'sn', 'mailuserquota' ] if len(username.split('@')) == 2: filter = 'mail=' + username else: filter = 'uid=' + username result = ldap.search('ou=users,dc=yunohost,dc=org', filter, user_attrs) if result: user = result[0] else: raise YunohostError('user_unknown', user=username) result_dict = { 'username': user['uid'][0], 'fullname': user['cn'][0], 'firstname': user['givenName'][0], 'lastname': user['sn'][0], 'mail': user['mail'][0] } if len(user['mail']) > 1: result_dict['mail-aliases'] = user['mail'][1:] if len(user['maildrop']) > 1: result_dict['mail-forward'] = user['maildrop'][1:] if 'mailuserquota' in user: userquota = user['mailuserquota'][0] if isinstance(userquota, int): userquota = str(userquota) # Test if userquota is '0' or '0M' ( quota pattern is ^(\d+[bkMGT])|0$ ) is_limited = not re.match('0[bkMGT]?', userquota) storage_use = '?' if service_status("dovecot")["status"] != "running": logger.warning(m18n.n('mailbox_used_space_dovecot_down')) elif username not in user_permission_info( "mail.main")["corresponding_users"]: logger.warning(m18n.n('mailbox_disabled', user=username)) else: try: cmd = 'doveadm -f flow quota get -u %s' % user['uid'][0] cmd_result = check_output(cmd) except Exception as e: cmd_result = "" logger.warning("Failed to fetch quota info ... : %s " % str(e)) # Exemple of return value for cmd: # """Quota name=User quota Type=STORAGE Value=0 Limit=- %=0 # Quota name=User quota Type=MESSAGE Value=0 Limit=- %=0""" has_value = re.search(r'Value=(\d+)', cmd_result) if has_value: storage_use = int(has_value.group(1)) storage_use = _convertSize(storage_use) if is_limited: has_percent = re.search(r'%=(\d+)', cmd_result) if has_percent: percentage = int(has_percent.group(1)) storage_use += ' (%s%%)' % percentage result_dict['mailbox-quota'] = { 'limit': userquota if is_limited else m18n.n('unlimit'), 'use': storage_use } return result_dict
def domain_add(operation_logger, domain, dyndns=False): """ Create a custom domain Keyword argument: domain -- Domain name to add dyndns -- Subscribe to DynDNS """ from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf from yunohost.utils.ldap import _get_ldap_interface if domain.startswith("xmpp-upload."): raise YunohostError("domain_cannot_add_xmpp_upload") ldap = _get_ldap_interface() try: ldap.validate_uniqueness({"virtualdomain": domain}) except MoulinetteError: raise YunohostError("domain_exists") operation_logger.start() # Lower domain to avoid some edge cases issues # See: https://forum.yunohost.org/t/invalid-domain-causes-diagnosis-web-to-fail-fr-on-demand/11765 domain = domain.lower() # DynDNS domain if dyndns: # Do not allow to subscribe to multiple dyndns domains... if os.path.exists("/etc/cron.d/yunohost-dyndns"): raise YunohostError("domain_dyndns_already_subscribed") from yunohost.dyndns import dyndns_subscribe, _dyndns_provides # Check that this domain can effectively be provided by # dyndns.yunohost.org. (i.e. is it a nohost.me / noho.st) if not _dyndns_provides("dyndns.yunohost.org", domain): raise YunohostError("domain_dyndns_root_unknown") # Actually subscribe dyndns_subscribe(domain=domain) try: import yunohost.certificate yunohost.certificate._certificate_install_selfsigned([domain], False) attr_dict = { "objectClass": ["mailDomain", "top"], "virtualdomain": domain, } try: ldap.add("virtualdomain=%s,ou=domains" % domain, attr_dict) except Exception as e: raise YunohostError("domain_creation_failed", domain=domain, error=e) # Don't regen these conf if we're still in postinstall if os.path.exists("/etc/yunohost/installed"): # Sometime we have weird issues with the regenconf where some files # appears as manually modified even though they weren't touched ... # There are a few ideas why this happens (like backup/restore nginx # conf ... which we shouldnt do ...). This in turns creates funky # situation where the regenconf may refuse to re-create the conf # (when re-creating a domain..) # So here we force-clear the has out of the regenconf if it exists. # This is a pretty ad hoc solution and only applied to nginx # because it's one of the major service, but in the long term we # should identify the root of this bug... _force_clear_hashes(["/etc/nginx/conf.d/%s.conf" % domain]) regen_conf( names=["nginx", "metronome", "dnsmasq", "postfix", "rspamd"]) app_ssowatconf() except Exception: # Force domain removal silently try: domain_remove(domain, force=True) except Exception: pass raise hook_callback("post_domain_add", args=[domain]) logger.success(m18n.n("domain_created"))
def user_update(operation_logger, username, firstname=None, lastname=None, mail=None, change_password=None, add_mailforward=None, remove_mailforward=None, add_mailalias=None, remove_mailalias=None, mailbox_quota=None): """ Update user informations Keyword argument: lastname mail firstname add_mailalias -- Mail aliases to add remove_mailforward -- Mailforward addresses to remove username -- Username of user to update add_mailforward -- Mailforward addresses to add change_password -- New password to set remove_mailalias -- Mail aliases to remove """ from yunohost.domain import domain_list, _get_maindomain from yunohost.app import app_ssowatconf from yunohost.utils.password import assert_password_is_strong_enough from yunohost.utils.ldap import _get_ldap_interface from yunohost.hook import hook_callback domains = domain_list()['domains'] # Populate user informations ldap = _get_ldap_interface() attrs_to_fetch = ['givenName', 'sn', 'mail', 'maildrop'] result = ldap.search(base='ou=users,dc=yunohost,dc=org', filter='uid=' + username, attrs=attrs_to_fetch) if not result: raise YunohostError('user_unknown', user=username) user = result[0] env_dict = {"YNH_USER_USERNAME": username} # Get modifications from arguments new_attr_dict = {} if firstname: new_attr_dict['givenName'] = [firstname] # TODO: Validate new_attr_dict['cn'] = new_attr_dict['displayName'] = [ firstname + ' ' + user['sn'][0] ] env_dict["YNH_USER_FIRSTNAME"] = firstname if lastname: new_attr_dict['sn'] = [lastname] # TODO: Validate new_attr_dict['cn'] = new_attr_dict['displayName'] = [ user['givenName'][0] + ' ' + lastname ] env_dict["YNH_USER_LASTNAME"] = lastname if lastname and firstname: new_attr_dict['cn'] = new_attr_dict['displayName'] = [ firstname + ' ' + lastname ] # change_password is None if user_update is not called to change the password if change_password is not None: # when in the cli interface if the option to change the password is called # without a specified value, change_password will be set to the const 0. # In this case we prompt for the new password. if msettings.get('interface') == 'cli' and not change_password: change_password = msignals.prompt(m18n.n("ask_password"), True, True) # Ensure sufficiently complex password assert_password_is_strong_enough("user", change_password) new_attr_dict['userPassword'] = [_hash_user_password(change_password)] env_dict["YNH_USER_PASSWORD"] = change_password if mail: main_domain = _get_maindomain() aliases = [ 'root@' + main_domain, 'admin@' + main_domain, 'webmaster@' + main_domain, 'postmaster@' + main_domain, ] try: ldap.validate_uniqueness({'mail': mail}) except Exception as e: raise YunohostError('user_update_failed', user=username, error=e) if mail[mail.find('@') + 1:] not in domains: raise YunohostError('mail_domain_unknown', domain=mail[mail.find('@') + 1:]) if mail in aliases: raise YunohostError('mail_unavailable') del user['mail'][0] new_attr_dict['mail'] = [mail] + user['mail'] if add_mailalias: if not isinstance(add_mailalias, list): add_mailalias = [add_mailalias] for mail in add_mailalias: try: ldap.validate_uniqueness({'mail': mail}) except Exception as e: raise YunohostError('user_update_failed', user=username, error=e) if mail[mail.find('@') + 1:] not in domains: raise YunohostError('mail_domain_unknown', domain=mail[mail.find('@') + 1:]) user['mail'].append(mail) new_attr_dict['mail'] = user['mail'] if remove_mailalias: if not isinstance(remove_mailalias, list): remove_mailalias = [remove_mailalias] for mail in remove_mailalias: if len(user['mail']) > 1 and mail in user['mail'][1:]: user['mail'].remove(mail) else: raise YunohostError('mail_alias_remove_failed', mail=mail) new_attr_dict['mail'] = user['mail'] if 'mail' in new_attr_dict: env_dict["YNH_USER_MAILS"] = ','.join(new_attr_dict['mail']) if add_mailforward: if not isinstance(add_mailforward, list): add_mailforward = [add_mailforward] for mail in add_mailforward: if mail in user['maildrop'][1:]: continue user['maildrop'].append(mail) new_attr_dict['maildrop'] = user['maildrop'] if remove_mailforward: if not isinstance(remove_mailforward, list): remove_mailforward = [remove_mailforward] for mail in remove_mailforward: if len(user['maildrop']) > 1 and mail in user['maildrop'][1:]: user['maildrop'].remove(mail) else: raise YunohostError('mail_forward_remove_failed', mail=mail) new_attr_dict['maildrop'] = user['maildrop'] if 'maildrop' in new_attr_dict: env_dict["YNH_USER_MAILFORWARDS"] = ','.join(new_attr_dict['maildrop']) if mailbox_quota is not None: new_attr_dict['mailuserquota'] = [mailbox_quota] env_dict["YNH_USER_MAILQUOTA"] = mailbox_quota operation_logger.start() try: ldap.update('uid=%s,ou=users' % username, new_attr_dict) except Exception as e: raise YunohostError('user_update_failed', user=username, error=e) # Trigger post_user_update hooks hook_callback('post_user_update', env=env_dict) logger.success(m18n.n('user_updated')) app_ssowatconf() return user_info(username)
def _update_ldap_group_permission(permission, allowed, label=None, show_tile=None, protected=None, sync_perm=True): """ Internal function that will rewrite user permission permission -- Name of the permission (e.g. mail or nextcloud or wordpress.editors) allowed -- (optional) A list of group/user to allow for the permission label -- (optional) Define a name for the permission. This label will be shown on the SSO and in the admin show_tile -- (optional) Define if a tile will be shown in the SSO protected -- (optional) Define if the permission can be added/removed to the visitor group Assumptions made, that should be checked before calling this function: - the permission does currently exists ... - the 'allowed' list argument is *different* from the current permission state ... otherwise ldap will miserably fail in such case... - the 'allowed' list contains *existing* groups. """ from yunohost.hook import hook_callback from yunohost.utils.ldap import _get_ldap_interface ldap = _get_ldap_interface() existing_permission = user_permission_info(permission) update = {} if allowed is not None: allowed = [allowed] if not isinstance(allowed, list) else allowed # Guarantee uniqueness of values in allowed, which would otherwise make ldap.update angry. allowed = set(allowed) update["groupPermission"] = [ "cn=" + g + ",ou=groups,dc=yunohost,dc=org" for g in allowed ] if label is not None: update["label"] = [str(label)] if protected is not None: update["isProtected"] = [str(protected).upper()] if show_tile is not None: if show_tile is True: if not existing_permission["url"]: logger.warning( m18n.n( "show_tile_cant_be_enabled_for_url_not_defined", permission=permission, )) show_tile = False elif existing_permission["url"].startswith("re:"): logger.warning( m18n.n("show_tile_cant_be_enabled_for_regex", permission=permission)) show_tile = False update["showTile"] = [str(show_tile).upper()] try: ldap.update("cn=%s,ou=permission" % permission, update) except Exception as e: raise YunohostError("permission_update_failed", permission=permission, error=e) # Trigger permission sync if asked if sync_perm: permission_sync_to_user() new_permission = user_permission_info(permission) # Trigger app callbacks app = permission.split(".")[0] sub_permission = permission.split(".")[1] old_corresponding_users = set(existing_permission["corresponding_users"]) new_corresponding_users = set(new_permission["corresponding_users"]) old_allowed_users = set(existing_permission["allowed"]) new_allowed_users = set(new_permission["allowed"]) effectively_added_users = new_corresponding_users - old_corresponding_users effectively_removed_users = old_corresponding_users - new_corresponding_users effectively_added_group = (new_allowed_users - old_allowed_users - effectively_added_users) effectively_removed_group = (old_allowed_users - new_allowed_users - effectively_removed_users) if effectively_added_users or effectively_added_group: hook_callback( "post_app_addaccess", args=[ app, ",".join(effectively_added_users), sub_permission, ",".join(effectively_added_group), ], ) if effectively_removed_users or effectively_removed_group: hook_callback( "post_app_removeaccess", args=[ app, ",".join(effectively_removed_users), sub_permission, ",".join(effectively_removed_group), ], ) return new_permission
def check_LDAP_db_integrity(): # Here we check that all attributes in all object are sychronized. # Here is the list of attributes per object: # user : memberOf, permission # group : member, permission # permission : groupPermission, inheritPermission # # The idea is to check that all attributes on all sides of object are sychronized. # One part should be done automatically by the "memberOf" overlay of LDAP. # The other part is done by the the "permission_sync_to_user" function of the permission module from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract ldap = _get_ldap_interface() user_search = ldap.search( 'ou=users,dc=yunohost,dc=org', '(&(objectclass=person)(!(uid=root))(!(uid=nobody)))', ['uid', 'memberOf', 'permission']) group_search = ldap.search('ou=groups,dc=yunohost,dc=org', '(objectclass=groupOfNamesYnh)', ['cn', 'member', 'memberUid', 'permission']) permission_search = ldap.search( 'ou=permission,dc=yunohost,dc=org', '(objectclass=permissionYnh)', ['cn', 'groupPermission', 'inheritPermission', 'memberUid']) user_map = {u['uid'][0]: u for u in user_search} group_map = {g['cn'][0]: g for g in group_search} permission_map = {p['cn'][0]: p for p in permission_search} for user in user_search: user_dn = 'uid=' + user['uid'][0] + ',ou=users,dc=yunohost,dc=org' group_list = [_ldap_path_extract(m, "cn") for m in user['memberOf']] permission_list = [ _ldap_path_extract(m, "cn") for m in user.get('permission', []) ] # This user's DN sould be found in all groups it is a member of for group in group_list: assert user_dn in group_map[group]['member'] # This user's DN should be found in all perms it has access to for permission in permission_list: assert user_dn in permission_map[permission]['inheritPermission'] for permission in permission_search: permission_dn = 'cn=' + permission['cn'][ 0] + ',ou=permission,dc=yunohost,dc=org' # inheritPermission uid's should match memberUids user_list = [ _ldap_path_extract(m, "uid") for m in permission.get('inheritPermission', []) ] assert set(user_list) == set(permission.get('memberUid', [])) # This perm's DN should be found on all related users it is related to for user in user_list: assert permission_dn in user_map[user]['permission'] # Same for groups : we should find the permission's DN for all related groups group_list = [ _ldap_path_extract(m, "cn") for m in permission.get('groupPermission', []) ] for group in group_list: assert permission_dn in group_map[group]['permission'] # The list of user in the group should be a subset of all users related to the current permission users_in_group = [ _ldap_path_extract(m, "uid") for m in group_map[group].get("member", []) ] assert set(users_in_group) <= set(user_list) for group in group_search: group_dn = 'cn=' + group['cn'][0] + ',ou=groups,dc=yunohost,dc=org' user_list = [ _ldap_path_extract(m, "uid") for m in group.get("member", []) ] # For primary groups, we should find that : # - len(user_list) is 1 (a primary group has only 1 member) # - the group name should be an existing yunohost user # - memberUid is empty (meaning no other member than the corresponding user) if group['cn'][0] in user_list: assert len(user_list) == 1 assert group["cn"][0] in user_map assert group.get('memberUid', []) == [] # Otherwise, user_list and memberUid should have the same content else: assert set(user_list) == set(group.get('memberUid', [])) # For all users members, this group should be in the "memberOf" on the other side for user in user_list: assert group_dn in user_map[user]['memberOf'] # For all the permissions of this group, the group should be among the "groupPermission" on the other side permission_list = [ _ldap_path_extract(m, "cn") for m in group.get('permission', []) ] for permission in permission_list: assert group_dn in permission_map[permission]['groupPermission'] # And the list of user of this group (user_list) should be a subset of all allowed users for this perm... allowed_user_list = [ _ldap_path_extract(m, "uid") for m in permission_map[permission].get( 'inheritPermission', []) ] assert set(user_list) <= set(allowed_user_list)