def subscription_valid(auth, username): """ Valid a subscription Keyword argument: username -- Must be unique """ from yunohost.user import user_create from yunohost.domain import domain_list from yunohost.app import app_ssowatconf info = subscription_info(auth, username) user_create(auth, info['username'], info['firstname'], info['lastname'], info['mail'], info['password']) #Update password new_attr_dict = {'userPassword': info['password']} if auth.update('uid=%s,ou=users' % username, new_attr_dict): app_ssowatconf(auth) else: raise MoulinetteError(169, m18n.n('user_creation_failed')) #Remove subscription from database cur = _get_db() cur.execute("DELETE FROM prefix_subscriptions WHERE `username`=%s", [username])
def user_delete(auth, username, purge=False): """ Delete user Keyword argument: username -- Username to delete purge """ from yunohost.app import app_ssowatconf from yunohost.hook import hook_callback if auth.remove('uid=%s,ou=users' % username): # Invalidate passwd to take user deletion into account subprocess.call(['nscd', '-i', 'passwd']) # Update SFTP user group memberlist = auth.search(filter='cn=sftpusers', attrs=['memberUid'])[0]['memberUid'] try: memberlist.remove(username) except: pass if auth.update('cn=sftpusers,ou=groups', { 'memberUid': memberlist }): if purge: subprocess.call(['rm', '-rf', '/home/{0}'.format(username)]) else: raise MoulinetteError(169, m18n.n('user_deletion_failed')) app_ssowatconf(auth) hook_callback('post_user_delete', args=[username, purge]) logger.success(m18n.n('user_deleted'))
def subscription_valid(auth, username): """ Valid a subscription Keyword argument: username -- Must be unique """ from yunohost.user import user_create from yunohost.domain import domain_list from yunohost.app import app_ssowatconf info=subscription_info(auth, username) user_create(auth, info['username'], info['firstname'], info['lastname'], info['mail'], info['password']) #Update password new_attr_dict = {'userPassword':info['password']} if auth.update('uid=%s,ou=users' % username, new_attr_dict): app_ssowatconf(auth) else: raise MoulinetteError(169, m18n.n('user_creation_failed')) #Remove subscription from database cur = _get_db() cur.execute("DELETE FROM prefix_subscriptions WHERE `username`=%s",[username])
def user_delete(auth, username, purge=False): """ Delete user Keyword argument: username -- Username to delete purge """ from yunohost.app import app_ssowatconf from yunohost.hook import hook_callback if auth.remove('uid=%s,ou=users' % username): # Invalidate passwd to take user deletion into account subprocess.call(['nscd', '-i', 'passwd']) # Update SFTP user group memberlist = auth.search(filter='cn=sftpusers', attrs=['memberUid'])[0]['memberUid'] try: memberlist.remove(username) except: pass if auth.update('cn=sftpusers,ou=groups', {'memberUid': memberlist}): if purge: subprocess.call(['rm', '-rf', '/home/{0}'.format(username)]) else: raise MoulinetteError(169, m18n.n('user_deletion_failed')) app_ssowatconf(auth) hook_callback('post_user_delete', args=[username, purge]) logger.success(m18n.n('user_deleted'))
def user_delete(auth, username, purge=False): """ Delete user Keyword argument: username -- Username to delete purge """ from yunohost.app import app_ssowatconf if auth.remove('uid=%s,ou=users' % username): # Update SFTP user group memberlist = auth.search(filter='cn=sftpusers', attrs=['memberUid'])[0]['memberUid'] try: memberlist.remove(username) except: pass if auth.update('cn=sftpusers,ou=groups', {'memberUid': memberlist}): if purge: os.system('rm -rf /home/%s' % username) else: raise MoulinetteError(169, m18n.n('user_deletion_failed')) app_ssowatconf(auth) msignals.display(m18n.n('user_deleted'), 'success')
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 domain_main_domain(operation_logger, new_main_domain=None): """ Check the current main domain, or change it Keyword argument: new_main_domain -- The new domain to be set as the main domain """ from yunohost.tools import _set_hostname # If no new domain specified, we return the current main domain if not new_main_domain: return {'current_main_domain': _get_maindomain()} # Check domain exists if new_main_domain not in domain_list()['domains']: raise YunohostError('domain_name_unknown', domain=new_main_domain) operation_logger.related_to.append(('domain', new_main_domain)) operation_logger.start() # Apply changes to ssl certs ssl_key = "/etc/ssl/private/yunohost_key.pem" ssl_crt = "/etc/ssl/private/yunohost_crt.pem" new_ssl_key = "/etc/yunohost/certs/%s/key.pem" % new_main_domain new_ssl_crt = "/etc/yunohost/certs/%s/crt.pem" % new_main_domain try: if os.path.exists(ssl_key) or os.path.lexists(ssl_key): os.remove(ssl_key) if os.path.exists(ssl_crt) or os.path.lexists(ssl_crt): os.remove(ssl_crt) os.symlink(new_ssl_key, ssl_key) os.symlink(new_ssl_crt, ssl_crt) _set_maindomain(new_main_domain) except Exception as e: logger.warning("%s" % e, exc_info=1) raise YunohostError('main_domain_change_failed') _set_hostname(new_main_domain) # Generate SSOwat configuration file app_ssowatconf() # Regen configurations try: with open('/etc/yunohost/installed', 'r'): regen_conf() except IOError: pass logger.success(m18n.n('main_domain_changed'))
def _configure_for_acme_challenge(auth, domain): nginx_conf_folder = "/etc/nginx/conf.d/%s.d" % domain nginx_conf_file = "%s/000-acmechallenge.conf" % nginx_conf_folder nginx_configuration = ''' location '/.well-known/acme-challenge' { default_type "text/plain"; alias %s; } ''' % WEBROOT_FOLDER # Check there isn't a conflicting file for the acme-challenge well-known # uri for path in glob.glob('%s/*.conf' % nginx_conf_folder): if path == nginx_conf_file: continue with open(path) as f: contents = f.read() if '/.well-known/acme-challenge' in contents: raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_conflicting_nginx_file', filepath=path)) # Write the conf if os.path.exists(nginx_conf_file): logger.info( "Nginx configuration file for ACME challenge already exists for domain, skipping.") return logger.info( "Adding Nginx configuration file for Acme challenge for domain %s.", domain) with open(nginx_conf_file, "w") as f: f.write(nginx_configuration) # Assume nginx conf is okay, and reload it # (FIXME : maybe add a check that it is, using nginx -t, haven't found # any clean function already implemented in yunohost to do this though) _run_service_command("reload", "nginx") app_ssowatconf(auth)
def domain_main_domain(operation_logger, new_main_domain=None): """ Check the current main domain, or change it Keyword argument: new_main_domain -- The new domain to be set as the main domain """ from yunohost.tools import _set_hostname # If no new domain specified, we return the current main domain if not new_main_domain: return {"current_main_domain": _get_maindomain()} # Check domain exists if new_main_domain not in domain_list()["domains"]: raise YunohostValidationError("domain_name_unknown", domain=new_main_domain) operation_logger.related_to.append(("domain", new_main_domain)) operation_logger.start() # Apply changes to ssl certs try: write_to_file("/etc/yunohost/current_host", new_main_domain) _set_hostname(new_main_domain) except Exception as e: logger.warning("%s" % e, exc_info=1) raise YunohostError("main_domain_change_failed") # Generate SSOwat configuration file app_ssowatconf() # Regen configurations if os.path.exists("/etc/yunohost/installed"): regen_conf() logger.success(m18n.n("main_domain_changed"))
def clean(): # Make sure we have a ssowat os.system("mkdir -p /etc/ssowat/") app_ssowatconf() test_apps = ["break_yo_system", "legacy_app", "legacy_app__2", "full_domain_app"] for test_app in test_apps: if _is_installed(test_app): app_remove(test_app) for filepath in glob.glob("/etc/nginx/conf.d/*.d/*%s*" % test_app): os.remove(filepath) for folderpath in glob.glob("/etc/yunohost/apps/*%s*" % test_app): shutil.rmtree(folderpath, ignore_errors=True) for folderpath in glob.glob("/var/www/*%s*" % test_app): shutil.rmtree(folderpath, ignore_errors=True) os.system( "bash -c \"mysql -u root --password=$(cat /etc/yunohost/mysql) 2>/dev/null <<< 'DROP DATABASE %s' \"" % test_app ) os.system( "bash -c \"mysql -u root --password=$(cat /etc/yunohost/mysql) 2>/dev/null <<< 'DROP USER %s@localhost'\"" % test_app ) os.system( "systemctl reset-failed nginx" ) # Reset failed quota for service to avoid running into start-limit rate ? os.system("systemctl start nginx") # Clean permissions for permission_name in user_permission_list(short=True)["permissions"]: if any(test_app in permission_name for test_app in test_apps): permission_delete(permission_name, force=True)
def user_delete(auth, username, purge=False): """ Delete user Keyword argument: username -- Username to delete purge """ from yunohost.app import app_ssowatconf if auth.remove('uid=%s,ou=users' % username): # Update SFTP user group memberlist = auth.search(filter='cn=sftpusers', attrs=['memberUid'])[0]['memberUid'] try: memberlist.remove(username) except: pass if auth.update('cn=sftpusers,ou=groups', { 'memberUid': memberlist }): if purge: os.system('rm -rf /home/%s' % username) else: raise MoulinetteError(169, m18n.n('user_deletion_failed')) app_ssowatconf(auth) msignals.display(m18n.n('user_deleted'), 'success')
def user_update(auth, 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 from yunohost.app import app_ssowatconf attrs_to_fetch = ['givenName', 'sn', 'mail', 'maildrop'] new_attr_dict = {} domains = domain_list(auth)['domains'] # Populate user informations result = auth.search(base='ou=users,dc=yunohost,dc=org', filter='uid=' + username, attrs=attrs_to_fetch) if not result: raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=username)) user = result[0] # Get modifications from arguments if firstname: new_attr_dict['givenName'] = firstname # TODO: Validate new_attr_dict['cn'] = new_attr_dict['displayName'] = firstname + ' ' + user['sn'][0] if lastname: new_attr_dict['sn'] = lastname # TODO: Validate new_attr_dict['cn'] = new_attr_dict['displayName'] = user['givenName'][0] + ' ' + lastname if lastname and firstname: new_attr_dict['cn'] = new_attr_dict['displayName'] = firstname + ' ' + lastname if change_password: char_set = string.ascii_uppercase + string.digits salt = ''.join(random.sample(char_set,8)) salt = '$1$' + salt + '$' new_attr_dict['userPassword'] = '******' + crypt.crypt(str(change_password), salt) if mail: auth.validate_uniqueness({ 'mail': mail }) if mail[mail.find('@')+1:] not in domains: raise MoulinetteError(errno.EINVAL, m18n.n('mail_domain_unknown', domain=mail[mail.find('@')+1:])) 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: auth.validate_uniqueness({ 'mail': mail }) if mail[mail.find('@')+1:] not in domains: raise MoulinetteError(errno.EINVAL, m18n.n('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 MoulinetteError(errno.EINVAL, m18n.n('mail_alias_remove_failed', mail=mail)) new_attr_dict['mail'] = user['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 MoulinetteError(errno.EINVAL, m18n.n('mail_forward_remove_failed', mail=mail)) new_attr_dict['maildrop'] = user['maildrop'] if mailbox_quota is not None: new_attr_dict['mailuserquota'] = mailbox_quota if auth.update('uid=%s,ou=users' % username, new_attr_dict): logger.success(m18n.n('user_updated')) app_ssowatconf(auth) return user_info(auth, username) else: raise MoulinetteError(169, m18n.n('user_update_failed'))
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 domain_remove(operation_logger, domain, remove_apps=False, force=False): """ Delete domains Keyword argument: domain -- Domain to delete remove_apps -- Remove applications installed on the domain force -- Force the domain removal and don't not ask confirmation to remove apps if remove_apps is specified """ from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf, app_info, app_remove from yunohost.utils.ldap import _get_ldap_interface # the 'force' here is related to the exception happening in domain_add ... # we don't want to check the domain exists because the ldap add may have # failed if not force and domain not in domain_list()['domains']: raise YunohostError('domain_name_unknown', domain=domain) # Check domain is not the main domain if domain == _get_maindomain(): other_domains = domain_list()["domains"] other_domains.remove(domain) if other_domains: raise YunohostError( "domain_cannot_remove_main", domain=domain, other_domains="\n * " + ("\n * ".join(other_domains)), ) else: raise YunohostError("domain_cannot_remove_main_add_new_one", domain=domain) # Check if apps are installed on the domain apps_on_that_domain = [] for app in _installed_apps(): settings = _get_app_settings(app) label = app_info(app)["name"] if settings.get("domain") == domain: apps_on_that_domain.append( (app, " - %s \"%s\" on https://%s%s" % (app, label, domain, settings["path"]) if "path" in settings else app)) if apps_on_that_domain: if remove_apps: if msettings.get('interface') == "cli" and not force: answer = msignals.prompt(m18n.n( 'domain_remove_confirm_apps_removal', apps="\n".join([x[1] for x in apps_on_that_domain]), answers='y/N'), color="yellow") if answer.upper() != "Y": raise YunohostError("aborting") for app, _ in apps_on_that_domain: app_remove(app) else: raise YunohostError('domain_uninstall_app_first', apps="\n".join( [x[1] for x in apps_on_that_domain])) operation_logger.start() ldap = _get_ldap_interface() try: ldap.remove("virtualdomain=" + domain + ",ou=domains") except Exception as e: raise YunohostError("domain_deletion_failed", domain=domain, error=e) os.system("rm -rf /etc/yunohost/certs/%s" % domain) # 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]) # And in addition we even force-delete the file Otherwise, if the file was # manually modified, it may not get removed by the regenconf which leads to # catastrophic consequences of nginx breaking because it can't load the # cert file which disappeared etc.. if os.path.exists("/etc/nginx/conf.d/%s.conf" % domain): _process_regen_conf("/etc/nginx/conf.d/%s.conf" % domain, new_conf=None, save=True) regen_conf(names=["nginx", "metronome", "dnsmasq", "postfix"]) app_ssowatconf() hook_callback("post_domain_remove", args=[domain]) logger.success(m18n.n("domain_deleted"))
def user_create(auth, username, firstname, lastname, mail, password, mailbox_quota="0"): """ Create user Keyword argument: firstname lastname username -- Must be unique mail -- Main mail address must be unique password mailbox_quota -- Mailbox size quota """ import pwd from yunohost.domain import domain_list from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf # Validate uniqueness of username and mail in LDAP auth.validate_uniqueness({'uid': username, 'mail': mail}) # Validate uniqueness of username in system users try: pwd.getpwnam(username) except KeyError: pass else: raise MoulinetteError(errno.EEXIST, m18n.n('system_username_exists')) # Check that the mail domain exists if mail[mail.find('@') + 1:] not in domain_list(auth)['domains']: raise MoulinetteError( errno.EINVAL, m18n.n('mail_domain_unknown', domain=mail[mail.find('@') + 1:])) # Get random UID/GID uid_check = gid_check = 0 while uid_check == 0 and gid_check == 0: uid = str(random.randint(200, 99999)) uid_check = os.system("getent passwd %s" % uid) gid_check = os.system("getent group %s" % uid) # Adapt values for LDAP fullname = '%s %s' % (firstname, lastname) rdn = 'uid=%s,ou=users' % username char_set = string.ascii_uppercase + string.digits salt = ''.join(random.sample(char_set, 8)) salt = '$1$' + salt + '$' user_pwd = '{CRYPT}' + crypt.crypt(str(password), salt) attr_dict = { 'objectClass': ['mailAccount', 'inetOrgPerson', 'posixAccount'], 'givenName': firstname, 'sn': lastname, 'displayName': fullname, 'cn': fullname, 'uid': username, 'mail': mail, 'maildrop': username, 'mailuserquota': mailbox_quota, 'userPassword': user_pwd, 'gidNumber': uid, 'uidNumber': uid, 'homeDirectory': '/home/' + username, 'loginShell': '/bin/false' } # If it is the first user, add some aliases if not auth.search(base='ou=users,dc=yunohost,dc=org', filter='uid=*'): with open('/etc/yunohost/current_host') as f: main_domain = f.readline().rstrip() aliases = [ 'root@' + main_domain, 'admin@' + main_domain, 'webmaster@' + main_domain, 'postmaster@' + main_domain, ] attr_dict['mail'] = [attr_dict['mail']] + aliases # If exists, remove the redirection from the SSO try: with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) except ValueError as e: raise MoulinetteError( errno.EINVAL, m18n.n('ssowat_persistent_conf_read_error', error=e.strerror)) except IOError: ssowat_conf = {} if 'redirected_urls' in ssowat_conf and '/' in ssowat_conf[ 'redirected_urls']: del ssowat_conf['redirected_urls']['/'] try: with open('/etc/ssowat/conf.json.persistent', 'w+') as f: json.dump(ssowat_conf, f, sort_keys=True, indent=4) except IOError as e: raise MoulinetteError( errno.EPERM, m18n.n('ssowat_persistent_conf_write_error', error=e.strerror)) if auth.add(rdn, attr_dict): # Invalidate passwd to take user creation into account subprocess.call(['nscd', '-i', 'passwd']) # Update SFTP user group memberlist = auth.search(filter='cn=sftpusers', attrs=['memberUid'])[0]['memberUid'] memberlist.append(username) if auth.update('cn=sftpusers,ou=groups', {'memberUid': memberlist}): try: # Attempt to create user home folder subprocess.check_call(['su', '-', username, '-c', "''"]) except subprocess.CalledProcessError: if not os.path.isdir('/home/{0}'.format(username)): logger.warning(m18n.n('user_home_creation_failed'), exc_info=1) app_ssowatconf(auth) # TODO: Send a welcome mail to user logger.success(m18n.n('user_created')) hook_callback('post_user_create', args=[username, mail, password, firstname, lastname]) return {'fullname': fullname, 'username': username, 'mail': mail} raise MoulinetteError(169, m18n.n('user_creation_failed'))
def user_update(auth, 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 from yunohost.app import app_ssowatconf attrs_to_fetch = ['givenName', 'sn', 'mail', 'maildrop'] new_attr_dict = {} domains = domain_list(auth)['domains'] # Populate user informations result = auth.search(base='ou=users,dc=yunohost,dc=org', filter='uid=' + username, attrs=attrs_to_fetch) if not result: raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=username)) user = result[0] # Get modifications from arguments if firstname: new_attr_dict['givenName'] = firstname # TODO: Validate new_attr_dict['cn'] = new_attr_dict[ 'displayName'] = firstname + ' ' + user['sn'][0] if lastname: new_attr_dict['sn'] = lastname # TODO: Validate new_attr_dict['cn'] = new_attr_dict[ 'displayName'] = user['givenName'][0] + ' ' + lastname if lastname and firstname: new_attr_dict['cn'] = new_attr_dict[ 'displayName'] = firstname + ' ' + lastname if change_password: char_set = string.ascii_uppercase + string.digits salt = ''.join(random.sample(char_set, 8)) salt = '$1$' + salt + '$' new_attr_dict['userPassword'] = '******' + crypt.crypt( str(change_password), salt) if mail: auth.validate_uniqueness({'mail': mail}) if mail[mail.find('@') + 1:] not in domains: raise MoulinetteError( errno.EINVAL, m18n.n('mail_domain_unknown', domain=mail[mail.find('@') + 1:])) 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: auth.validate_uniqueness({'mail': mail}) if mail[mail.find('@') + 1:] not in domains: raise MoulinetteError( errno.EINVAL, m18n.n('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 MoulinetteError( errno.EINVAL, m18n.n('mail_alias_remove_failed', mail=mail)) new_attr_dict['mail'] = user['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 MoulinetteError( errno.EINVAL, m18n.n('mail_forward_remove_failed', mail=mail)) new_attr_dict['maildrop'] = user['maildrop'] if mailbox_quota is not None: new_attr_dict['mailuserquota'] = mailbox_quota if auth.update('uid=%s,ou=users' % username, new_attr_dict): logger.success(m18n.n('user_updated')) app_ssowatconf(auth) return user_info(auth, username) else: raise MoulinetteError(169, m18n.n('user_update_failed'))
def tools_postinstall(domain, password, ignore_dyndns=False): """ YunoHost post-install Keyword argument: domain -- YunoHost main domain ignore_dyndns -- Do not subscribe domain to a DynDNS service password -- YunoHost admin password """ dyndns = not ignore_dyndns # Do some checks at first if os.path.isfile('/etc/yunohost/installed'): raise MoulinetteError(errno.EPERM, m18n.n('yunohost_already_installed')) if len(domain.split('.')) >= 3 and not ignore_dyndns: try: r = requests.get('https://dyndns.yunohost.org/domains') except requests.ConnectionError: pass else: dyndomains = json.loads(r.text) dyndomain = '.'.join(domain.split('.')[1:]) if dyndomain in dyndomains: if requests.get('https://dyndns.yunohost.org/test/%s' % domain).status_code == 200: dyndns = True else: raise MoulinetteError(errno.EEXIST, m18n.n('dyndns_unavailable')) logger.info(m18n.n('yunohost_installing')) # Instantiate LDAP Authenticator auth = init_authenticator(('ldap', 'default'), {'uri': "ldap://localhost:389", 'base_dn': "dc=yunohost,dc=org", 'user_rdn': "cn=admin" }) auth.authenticate('yunohost') # Initialize LDAP for YunoHost # TODO: Improve this part by integrate ldapinit into conf_regen hook tools_ldapinit(auth) # Create required folders folders_to_create = [ '/etc/yunohost/apps', '/etc/yunohost/certs', '/var/cache/yunohost/repo', '/home/yunohost.backup', '/home/yunohost.app' ] for folder in folders_to_create: try: os.listdir(folder) except OSError: os.makedirs(folder) # Change folders permissions os.system('chmod 755 /home/yunohost.app') # Set hostname to avoid amavis bug if os.system('hostname -d') != 0: os.system('hostname yunohost.yunohost.org') # Add a temporary SSOwat rule to redirect SSO to admin page try: with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) except IOError: ssowat_conf = {} if 'redirected_urls' not in ssowat_conf: ssowat_conf['redirected_urls'] = {} ssowat_conf['redirected_urls']['/'] = domain +'/yunohost/admin' with open('/etc/ssowat/conf.json.persistent', 'w+') as f: json.dump(ssowat_conf, f, sort_keys=True, indent=4) os.system('chmod 644 /etc/ssowat/conf.json.persistent') # Create SSL CA service_regen_conf(['ssl'], force=True) ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' command_list = [ 'echo "01" > %s/serial' % ssl_dir, 'rm %s/index.txt' % ssl_dir, 'touch %s/index.txt' % ssl_dir, 'cp %s/openssl.cnf %s/openssl.ca.cnf' % (ssl_dir, ssl_dir), 'sed -i "s/yunohost.org/%s/g" %s/openssl.ca.cnf ' % (domain, ssl_dir), 'openssl req -x509 -new -config %s/openssl.ca.cnf -days 3650 -out %s/ca/cacert.pem -keyout %s/ca/cakey.pem -nodes -batch' % (ssl_dir, ssl_dir, ssl_dir), 'cp %s/ca/cacert.pem /etc/ssl/certs/ca-yunohost_crt.pem' % ssl_dir, 'update-ca-certificates' ] for command in command_list: if os.system(command) != 0: raise MoulinetteError(errno.EPERM, m18n.n('yunohost_ca_creation_failed')) # New domain config tools_maindomain(auth, old_domain='yunohost.org', new_domain=domain, dyndns=dyndns) # Generate SSOwat configuration file app_ssowatconf(auth) # Change LDAP admin password tools_adminpw(auth, password) # Enable UPnP silently and reload firewall firewall_upnp('enable', no_refresh=True) os.system('touch /etc/yunohost/installed') # Enable and start YunoHost firewall at boot time os.system('update-rc.d yunohost-firewall enable') os.system('service yunohost-firewall start') service_regen_conf(force=True) logger.success(m18n.n('yunohost_configured'))
def tools_postinstall(domain, password, ignore_dyndns=False): """ YunoHost post-install Keyword argument: domain -- YunoHost main domain ignore_dyndns -- Do not subscribe domain to a DynDNS service (only needed for nohost.me, noho.st domains) password -- YunoHost admin password """ dyndns = not ignore_dyndns # Do some checks at first if os.path.isfile('/etc/yunohost/installed'): raise MoulinetteError(errno.EPERM, m18n.n('yunohost_already_installed')) if len(domain.split('.')) >= 3 and not ignore_dyndns: try: r = requests.get('https://dyndns.yunohost.org/domains') except requests.ConnectionError: pass else: dyndomains = json.loads(r.text) dyndomain = '.'.join(domain.split('.')[1:]) if dyndomain in dyndomains: if requests.get('https://dyndns.yunohost.org/test/%s' % domain).status_code == 200: dyndns = True else: raise MoulinetteError(errno.EEXIST, m18n.n('dyndns_unavailable')) else: dyndns = False else: dyndns = False logger.info(m18n.n('yunohost_installing')) # Initialize LDAP for YunoHost # TODO: Improve this part by integrate ldapinit into conf_regen hook auth = tools_ldapinit() # Create required folders folders_to_create = [ '/etc/yunohost/apps', '/etc/yunohost/certs', '/var/cache/yunohost/repo', '/home/yunohost.backup', '/home/yunohost.app' ] for folder in folders_to_create: try: os.listdir(folder) except OSError: os.makedirs(folder) # Change folders permissions os.system('chmod 755 /home/yunohost.app') # Set hostname to avoid amavis bug if os.system('hostname -d') != 0: os.system('hostname yunohost.yunohost.org') # Add a temporary SSOwat rule to redirect SSO to admin page try: with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) except ValueError as e: raise MoulinetteError( errno.EINVAL, m18n.n('ssowat_persistent_conf_read_error', error=e.strerror)) except IOError: ssowat_conf = {} if 'redirected_urls' not in ssowat_conf: ssowat_conf['redirected_urls'] = {} ssowat_conf['redirected_urls']['/'] = domain + '/yunohost/admin' try: with open('/etc/ssowat/conf.json.persistent', 'w+') as f: json.dump(ssowat_conf, f, sort_keys=True, indent=4) except IOError as e: raise MoulinetteError( errno.EPERM, m18n.n('ssowat_persistent_conf_write_error', error=e.strerror)) os.system('chmod 644 /etc/ssowat/conf.json.persistent') # Create SSL CA service_regen_conf(['ssl'], force=True) ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' command_list = [ 'echo "01" > %s/serial' % ssl_dir, 'rm %s/index.txt' % ssl_dir, 'touch %s/index.txt' % ssl_dir, 'cp %s/openssl.cnf %s/openssl.ca.cnf' % (ssl_dir, ssl_dir), 'sed -i "s/yunohost.org/%s/g" %s/openssl.ca.cnf ' % (domain, ssl_dir), 'openssl req -x509 -new -config %s/openssl.ca.cnf -days 3650 -out %s/ca/cacert.pem -keyout %s/ca/cakey.pem -nodes -batch' % (ssl_dir, ssl_dir, ssl_dir), 'cp %s/ca/cacert.pem /etc/ssl/certs/ca-yunohost_crt.pem' % ssl_dir, 'update-ca-certificates' ] for command in command_list: if os.system(command) != 0: raise MoulinetteError(errno.EPERM, m18n.n('yunohost_ca_creation_failed')) # New domain config domain_add(auth, domain, dyndns) tools_maindomain(auth, domain) # Generate SSOwat configuration file app_ssowatconf(auth) # Change LDAP admin password tools_adminpw(auth, password) # Enable UPnP silently and reload firewall firewall_upnp('enable', no_refresh=True) os.system('touch /etc/yunohost/installed') # Enable and start YunoHost firewall at boot time os.system('update-rc.d yunohost-firewall enable') os.system('service yunohost-firewall start') service_regen_conf(force=True) logger.success(m18n.n('yunohost_configured'))
def domain_remove(operation_logger, domain, force=False): """ Delete domains Keyword argument: domain -- Domain to delete force -- Force the domain removal """ from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf, app_info from yunohost.utils.ldap import _get_ldap_interface if not force and domain not in domain_list()['domains']: raise YunohostError('domain_name_unknown', domain=domain) # Check domain is not the main domain if domain == _get_maindomain(): other_domains = domain_list()["domains"] other_domains.remove(domain) if other_domains: raise YunohostError('domain_cannot_remove_main', domain=domain, other_domains="\n * " + ("\n * ".join(other_domains))) else: raise YunohostError('domain_cannot_remove_main_add_new_one', domain=domain) # Check if apps are installed on the domain apps_on_that_domain = [] for app in _installed_apps(): settings = _get_app_settings(app) label = app_info(app)["name"] if settings.get("domain") == domain: apps_on_that_domain.append(" - %s \"%s\" on https://%s%s" % (app, label, domain, settings["path"]) if "path" in settings else app) if apps_on_that_domain: raise YunohostError('domain_uninstall_app_first', apps="\n".join(apps_on_that_domain)) operation_logger.start() ldap = _get_ldap_interface() try: ldap.remove('virtualdomain=' + domain + ',ou=domains') except Exception as e: raise YunohostError('domain_deletion_failed', domain=domain, error=e) os.system('rm -rf /etc/yunohost/certs/%s' % domain) # 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]) # And in addition we even force-delete the file Otherwise, if the file was # manually modified, it may not get removed by the regenconf which leads to # catastrophic consequences of nginx breaking because it can't load the # cert file which disappeared etc.. if os.path.exists("/etc/nginx/conf.d/%s.conf" % domain): _process_regen_conf("/etc/nginx/conf.d/%s.conf" % domain, new_conf=None, save=True) regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix']) app_ssowatconf() hook_callback('post_domain_remove', args=[domain]) logger.success(m18n.n('domain_deleted'))
def reset_ssowat_conf(): # Make sure we have a ssowat os.system("mkdir -p /etc/ssowat/") app_ssowatconf()
def tools_postinstall(domain, password, dyndns=False): """ YunoHost post-install Keyword argument: domain -- YunoHost main domain dyndns -- Subscribe domain to a DynDNS service password -- YunoHost admin password """ from moulinette.core import init_authenticator from yunohost.backup import backup_init from yunohost.app import app_ssowatconf from yunohost.firewall import firewall_upnp, firewall_reload try: with open('/etc/yunohost/installed') as f: pass except IOError: msignals.display(m18n.n('yunohost_installing')) else: raise MoulinetteError(errno.EPERM, m18n.n('yunohost_already_installed')) if len(domain.split('.')) >= 3: try: r = requests.get('http://dyndns.yunohost.org/domains') except ConnectionError: pass else: dyndomains = json.loads(r.text) dyndomain = '.'.join(domain.split('.')[1:]) if dyndomain in dyndomains: if requests.get('http://dyndns.yunohost.org/test/%s' % domain).status_code == 200: dyndns=True else: raise MoulinetteError(errno.EEXIST, m18n.n('dyndns_unavailable')) # Create required folders folders_to_create = [ '/etc/yunohost/apps', '/etc/yunohost/certs', '/var/cache/yunohost/repo', '/home/yunohost.backup', '/home/yunohost.app' ] for folder in folders_to_create: try: os.listdir(folder) except OSError: os.makedirs(folder) # Set hostname to avoid amavis bug if os.system('hostname -d') != 0: os.system('hostname yunohost.yunohost.org') # Add a temporary SSOwat rule to redirect SSO to admin page try: with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) except IOError: ssowat_conf = {} if 'redirected_urls' not in ssowat_conf: ssowat_conf['redirected_urls'] = {} ssowat_conf['redirected_urls']['/'] = domain +'/yunohost/admin' with open('/etc/ssowat/conf.json.persistent', 'w+') as f: json.dump(ssowat_conf, f, sort_keys=True, indent=4) os.system('chmod 644 /etc/ssowat/conf.json.persistent') # Create SSL CA ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' command_list = [ 'echo "01" > %s/serial' % ssl_dir, 'rm %s/index.txt' % ssl_dir, 'touch %s/index.txt' % ssl_dir, 'cp %s/openssl.cnf %s/openssl.ca.cnf' % (ssl_dir, ssl_dir), 'sed -i "s/yunohost.org/%s/g" %s/openssl.ca.cnf ' % (domain, ssl_dir), 'openssl req -x509 -new -config %s/openssl.ca.cnf -days 3650 -out %s/ca/cacert.pem -keyout %s/ca/cakey.pem -nodes -batch' % (ssl_dir, ssl_dir, ssl_dir), 'cp %s/ca/cacert.pem /etc/ssl/certs/ca-yunohost_crt.pem' % ssl_dir, 'update-ca-certificates' ] for command in command_list: if os.system(command) != 0: raise MoulinetteError(errno.EPERM, m18n.n('yunohost_ca_creation_failed')) # Instantiate LDAP Authenticator auth = init_authenticator(('ldap', 'default'), { 'uri': "ldap://localhost:389", 'base_dn': "dc=yunohost,dc=org", 'user_rdn': "cn=admin" }) auth.authenticate('yunohost') # Initialize YunoHost LDAP base tools_ldapinit(auth) # Initialize backup system backup_init() # New domain config tools_maindomain(auth, old_domain='yunohost.org', new_domain=domain, dyndns=dyndns) # Generate SSOwat configuration file app_ssowatconf(auth) # Change LDAP admin password tools_adminpw(old_password='******', new_password=password) # Enable uPnP firewall_upnp(action=['enable']) try: firewall_reload() except MoulinetteError: firewall_upnp(action=['disable']) os.system('touch /etc/yunohost/installed') msignals.display(m18n.n('yunohost_configured'), 'success')
def backup_restore(auth, name, hooks=[], ignore_hooks=False, apps=[], ignore_apps=False, force=False): """ Restore from a local backup archive Keyword argument: name -- Name of the local backup archive hooks -- List of restoration hooks names to execute ignore_hooks -- Do not execute backup hooks apps -- List of application names to restore ignore_apps -- Do not restore apps force -- Force restauration on an already installed system """ # Validate what to restore if ignore_hooks and ignore_apps: raise MoulinetteError(errno.EINVAL, m18n.n('restore_action_required')) # Retrieve and open the archive info = backup_info(name) archive_file = info['path'] try: tar = tarfile.open(archive_file, "r:gz") except: logger.debug("cannot open backup archive '%s'", archive_file, exc_info=1) raise MoulinetteError(errno.EIO, m18n.n('backup_archive_open_failed')) # Check temporary directory tmp_dir = "%s/tmp/%s" % (backup_path, name) if os.path.isdir(tmp_dir): logger.debug("temporary directory for restoration '%s' already exists", tmp_dir) os.system('rm -rf %s' % tmp_dir) # Check available disk space statvfs = os.statvfs(backup_path) free_space = statvfs.f_frsize * statvfs.f_bavail if free_space < info['size']: logger.debug("%dB left but %dB is needed", free_space, info['size']) raise MoulinetteError( errno.EIO, m18n.n('not_enough_disk_space', path=backup_path)) def _clean_tmp_dir(retcode=0): ret = hook_callback('post_backup_restore', args=[tmp_dir, retcode]) if not ret['failed']: filesystem.rm(tmp_dir, True, True) else: logger.warning(m18n.n('restore_cleaning_failed')) # Extract the tarball logger.info(m18n.n('backup_extracting_archive')) tar.extractall(tmp_dir) tar.close() # Retrieve backup info info_file = "%s/info.json" % tmp_dir try: with open(info_file, 'r') as f: info = json.load(f) except IOError: logger.debug("unable to load '%s'", info_file, exc_info=1) raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) else: logger.debug("restoring from backup '%s' created on %s", name, time.ctime(info['created_at'])) # Initialize restauration summary result result = { 'apps': [], 'hooks': {}, } # Check if YunoHost is installed if os.path.isfile('/etc/yunohost/installed'): logger.warning(m18n.n('yunohost_already_installed')) if not force: try: # Ask confirmation for restoring i = msignals.prompt(m18n.n('restore_confirm_yunohost_installed', answers='y/N')) except NotImplemented: pass else: if i == 'y' or i == 'Y': force = True if not force: _clean_tmp_dir() raise MoulinetteError(errno.EEXIST, m18n.n('restore_failed')) else: # Retrieve the domain from the backup try: with open("%s/conf/ynh/current_host" % tmp_dir, 'r') as f: domain = f.readline().rstrip() except IOError: logger.debug("unable to retrieve current_host from the backup", exc_info=1) raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) logger.debug("executing the post-install...") tools_postinstall(domain, 'yunohost', True) # Run system hooks if not ignore_hooks: # Filter hooks to execute hooks_list = set(info['hooks'].keys()) _is_hook_in_backup = lambda h: True if hooks: def _is_hook_in_backup(h): if h in hooks_list: return True logger.error(m18n.n('backup_archive_hook_not_exec', hook=h)) return False else: hooks = hooks_list # Check hooks availibility hooks_filtered = set() for h in hooks: if not _is_hook_in_backup(h): continue try: hook_info('restore', h) except: tmp_hooks = glob('{:s}/hooks/restore/*-{:s}'.format(tmp_dir, h)) if not tmp_hooks: logger.exception(m18n.n('restore_hook_unavailable', hook=h)) continue # Add restoration hook from the backup to the system # FIXME: Refactor hook_add and use it instead restore_hook_folder = custom_hook_folder + 'restore' filesystem.mkdir(restore_hook_folder, 755, True) for f in tmp_hooks: logger.debug("adding restoration hook '%s' to the system " "from the backup archive '%s'", f, archive_file) shutil.copy(f, restore_hook_folder) hooks_filtered.add(h) if hooks_filtered: logger.info(m18n.n('restore_running_hooks')) ret = hook_callback('restore', hooks_filtered, args=[tmp_dir]) result['hooks'] = ret['succeed'] # Add apps restore hook if not ignore_apps: # Filter applications to restore apps_list = set(info['apps'].keys()) apps_filtered = set() if apps: for a in apps: if a not in apps_list: logger.error(m18n.n('backup_archive_app_not_found', app=a)) else: apps_filtered.add(a) else: apps_filtered = apps_list for app_instance_name in apps_filtered: tmp_app_dir = '{:s}/apps/{:s}'.format(tmp_dir, app_instance_name) tmp_app_bkp_dir = tmp_app_dir + '/backup' # Check if the app is not already installed if _is_installed(app_instance_name): logger.error(m18n.n('restore_already_installed_app', app=app_instance_name)) continue # Check if the app has a restore script app_script = tmp_app_dir + '/settings/scripts/restore' if not os.path.isfile(app_script): logger.warning(m18n.n('unrestore_app', app=app_instance_name)) continue tmp_script = '/tmp/restore_' + app_instance_name app_setting_path = '/etc/yunohost/apps/' + app_instance_name logger.info(m18n.n('restore_running_app_script', app=app_instance_name)) try: # Copy app settings and set permissions shutil.copytree(tmp_app_dir + '/settings', app_setting_path) filesystem.chmod(app_setting_path, 0555, 0444, True) filesystem.chmod(app_setting_path + '/settings.yml', 0400) # Execute app restore script subprocess.call(['install', '-Dm555', app_script, tmp_script]) # Prepare env. var. to pass to script env_dict = {} app_id, app_instance_nb = _parse_app_instance_name(app_instance_name) env_dict["YNH_APP_ID"] = app_id env_dict["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) env_dict["YNH_APP_BACKUP_DIR"] = tmp_app_bkp_dir hook_exec(tmp_script, args=[tmp_app_bkp_dir, app_instance_name], raise_on_error=True, chdir=tmp_app_bkp_dir, env=env_dict) except: logger.exception(m18n.n('restore_app_failed', app=app_instance_name)) # Cleaning app directory shutil.rmtree(app_setting_path, ignore_errors=True) else: result['apps'].append(app_instance_name) finally: filesystem.rm(tmp_script, force=True) # Check if something has been restored if not result['hooks'] and not result['apps']: _clean_tmp_dir(1) raise MoulinetteError(errno.EINVAL, m18n.n('restore_nothings_done')) if result['apps']: app_ssowatconf(auth) _clean_tmp_dir() logger.success(m18n.n('restore_complete')) return result
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 user_create(auth, username, firstname, lastname, mail, password, mailbox_quota=0): """ Create user Keyword argument: firstname lastname username -- Must be unique mail -- Main mail address must be unique password mailbox_quota -- Mailbox size quota """ import pwd from yunohost.domain import domain_list from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf # Validate uniqueness of username and mail in LDAP auth.validate_uniqueness({ 'uid' : username, 'mail' : mail }) # Validate uniqueness of username in system users try: pwd.getpwnam(username) except KeyError: pass else: raise MoulinetteError(errno.EEXIST, m18n.n('system_username_exists')) # Check that the mail domain exists if mail[mail.find('@')+1:] not in domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('mail_domain_unknown', domain=mail[mail.find('@')+1:])) # Get random UID/GID uid_check = gid_check = 0 while uid_check == 0 and gid_check == 0: uid = str(random.randint(200, 99999)) uid_check = os.system("getent passwd %s" % uid) gid_check = os.system("getent group %s" % uid) # Adapt values for LDAP fullname = '%s %s' % (firstname, lastname) rdn = 'uid=%s,ou=users' % username char_set = string.ascii_uppercase + string.digits salt = ''.join(random.sample(char_set,8)) salt = '$1$' + salt + '$' user_pwd = '{CRYPT}' + crypt.crypt(str(password), salt) attr_dict = { 'objectClass' : ['mailAccount', 'inetOrgPerson', 'posixAccount'], 'givenName' : firstname, 'sn' : lastname, 'displayName' : fullname, 'cn' : fullname, 'uid' : username, 'mail' : mail, 'maildrop' : username, 'mailuserquota' : mailbox_quota, 'userPassword' : user_pwd, 'gidNumber' : uid, 'uidNumber' : uid, 'homeDirectory' : '/home/' + username, 'loginShell' : '/bin/false' } # If it is the first user, add some aliases if not auth.search(base='ou=users,dc=yunohost,dc=org', filter='uid=*'): with open('/etc/yunohost/current_host') as f: main_domain = f.readline().rstrip() aliases = [ 'root@'+ main_domain, 'admin@'+ main_domain, 'webmaster@'+ main_domain, 'postmaster@'+ main_domain, ] attr_dict['mail'] = [ attr_dict['mail'] ] + aliases # If exists, remove the redirection from the SSO try: with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) if 'redirected_urls' in ssowat_conf and '/' in ssowat_conf['redirected_urls']: del ssowat_conf['redirected_urls']['/'] with open('/etc/ssowat/conf.json.persistent', 'w+') as f: json.dump(ssowat_conf, f, sort_keys=True, indent=4) except IOError: pass if auth.add(rdn, attr_dict): # Invalidate passwd to take user creation into account subprocess.call(['nscd', '-i', 'passwd']) # Update SFTP user group memberlist = auth.search(filter='cn=sftpusers', attrs=['memberUid'])[0]['memberUid'] memberlist.append(username) if auth.update('cn=sftpusers,ou=groups', { 'memberUid': memberlist }): try: # Attempt to create user home folder subprocess.check_call( ['su', '-', username, '-c', "''"]) except subprocess.CalledProcessError: if not os.path.isdir('/home/{0}'.format(username)): logger.warning(m18n.n('user_home_creation_failed'), exc_info=1) app_ssowatconf(auth) #TODO: Send a welcome mail to user logger.success(m18n.n('user_created')) hook_callback('post_user_create', args=[username, mail, password, firstname, lastname]) return { 'fullname' : fullname, 'username' : username, 'mail' : mail } raise MoulinetteError(169, m18n.n('user_creation_failed'))
def backup_restore(auth, name, hooks=[], ignore_hooks=False, apps=[], ignore_apps=False, force=False): """ Restore from a local backup archive Keyword argument: name -- Name of the local backup archive hooks -- List of restoration hooks names to execute ignore_hooks -- Do not execute backup hooks apps -- List of application names to restore ignore_apps -- Do not restore apps force -- Force restauration on an already installed system """ # Validate what to restore if ignore_hooks and ignore_apps: raise MoulinetteError(errno.EINVAL, m18n.n('restore_action_required')) # Retrieve and open the archive info = backup_info(name) archive_file = info['path'] try: tar = tarfile.open(archive_file, "r:gz") except: logger.debug("cannot open backup archive '%s'", archive_file, exc_info=1) raise MoulinetteError(errno.EIO, m18n.n('backup_archive_open_failed')) # Check temporary directory tmp_dir = "%s/tmp/%s" % (backup_path, name) if os.path.isdir(tmp_dir): logger.debug("temporary directory for restoration '%s' already exists", tmp_dir) os.system('rm -rf %s' % tmp_dir) # Check available disk space statvfs = os.statvfs(backup_path) free_space = statvfs.f_frsize * statvfs.f_bavail if free_space < info['size']: logger.debug("%dB left but %dB is needed", free_space, info['size']) raise MoulinetteError( errno.EIO, m18n.n('not_enough_disk_space', path=backup_path)) def _clean_tmp_dir(retcode=0): ret = hook_callback('post_backup_restore', args=[tmp_dir, retcode]) if not ret['failed']: filesystem.rm(tmp_dir, True, True) else: logger.warning(m18n.n('restore_cleaning_failed')) # Extract the tarball logger.info(m18n.n('backup_extracting_archive')) tar.extractall(tmp_dir) tar.close() # Retrieve backup info info_file = "%s/info.json" % tmp_dir try: with open(info_file, 'r') as f: info = json.load(f) except IOError: logger.debug("unable to load '%s'", info_file, exc_info=1) raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) else: logger.debug("restoring from backup '%s' created on %s", name, time.ctime(info['created_at'])) # Initialize restauration summary result result = { 'apps': [], 'hooks': {}, } # Check if YunoHost is installed if os.path.isfile('/etc/yunohost/installed'): logger.warning(m18n.n('yunohost_already_installed')) if not force: try: # Ask confirmation for restoring i = msignals.prompt( m18n.n('restore_confirm_yunohost_installed', answers='y/N')) except NotImplemented: pass else: if i == 'y' or i == 'Y': force = True if not force: _clean_tmp_dir() raise MoulinetteError(errno.EEXIST, m18n.n('restore_failed')) else: # Retrieve the domain from the backup try: with open("%s/conf/ynh/current_host" % tmp_dir, 'r') as f: domain = f.readline().rstrip() except IOError: logger.debug("unable to retrieve current_host from the backup", exc_info=1) raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) logger.debug("executing the post-install...") tools_postinstall(domain, 'yunohost', True) # Run system hooks if not ignore_hooks: # Filter hooks to execute hooks_list = set(info['hooks'].keys()) _is_hook_in_backup = lambda h: True if hooks: def _is_hook_in_backup(h): if h in hooks_list: return True logger.error(m18n.n('backup_archive_hook_not_exec', hook=h)) return False else: hooks = hooks_list # Check hooks availibility hooks_filtered = set() for h in hooks: if not _is_hook_in_backup(h): continue try: hook_info('restore', h) except: tmp_hooks = glob('{:s}/hooks/restore/*-{:s}'.format( tmp_dir, h)) if not tmp_hooks: logger.exception(m18n.n('restore_hook_unavailable', hook=h)) continue # Add restoration hook from the backup to the system # FIXME: Refactor hook_add and use it instead restore_hook_folder = custom_hook_folder + 'restore' filesystem.mkdir(restore_hook_folder, 755, True) for f in tmp_hooks: logger.debug( "adding restoration hook '%s' to the system " "from the backup archive '%s'", f, archive_file) shutil.copy(f, restore_hook_folder) hooks_filtered.add(h) if hooks_filtered: logger.info(m18n.n('restore_running_hooks')) ret = hook_callback('restore', hooks_filtered, args=[tmp_dir]) result['hooks'] = ret['succeed'] # Add apps restore hook if not ignore_apps: # Filter applications to restore apps_list = set(info['apps'].keys()) apps_filtered = set() if apps: for a in apps: if a not in apps_list: logger.error(m18n.n('backup_archive_app_not_found', app=a)) else: apps_filtered.add(a) else: apps_filtered = apps_list for app_instance_name in apps_filtered: tmp_app_dir = '{:s}/apps/{:s}'.format(tmp_dir, app_instance_name) tmp_app_bkp_dir = tmp_app_dir + '/backup' # Parse app instance name and id # TODO: Use app_id to check if app is installed? app_id, app_instance_nb = _parse_app_instance_name( app_instance_name) # Check if the app is not already installed if _is_installed(app_instance_name): logger.error( m18n.n('restore_already_installed_app', app=app_instance_name)) continue # Check if the app has a restore script app_script = tmp_app_dir + '/settings/scripts/restore' if not os.path.isfile(app_script): logger.warning(m18n.n('unrestore_app', app=app_instance_name)) continue tmp_script = '/tmp/restore_' + app_instance_name app_setting_path = '/etc/yunohost/apps/' + app_instance_name logger.info( m18n.n('restore_running_app_script', app=app_instance_name)) try: # Copy app settings and set permissions # TODO: Copy app hooks too shutil.copytree(tmp_app_dir + '/settings', app_setting_path) filesystem.chmod(app_setting_path, 0555, 0444, True) filesystem.chmod(app_setting_path + '/settings.yml', 0400) # Copy restore script in a tmp file subprocess.call(['install', '-Dm555', app_script, tmp_script]) # Prepare env. var. to pass to script env_dict = {} env_dict["YNH_APP_ID"] = app_id env_dict["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) env_dict["YNH_APP_BACKUP_DIR"] = tmp_app_bkp_dir # Execute app restore script hook_exec(tmp_script, args=[tmp_app_bkp_dir, app_instance_name], raise_on_error=True, chdir=tmp_app_bkp_dir, env=env_dict) except: logger.exception( m18n.n('restore_app_failed', app=app_instance_name)) # Copy remove script in a tmp file filesystem.rm(tmp_script, force=True) app_script = tmp_app_dir + '/settings/scripts/remove' tmp_script = '/tmp/remove_' + app_instance_name subprocess.call(['install', '-Dm555', app_script, tmp_script]) # Setup environment for remove script env_dict_remove = {} env_dict_remove["YNH_APP_ID"] = app_id env_dict_remove["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str( app_instance_nb) # Execute remove script # TODO: call app_remove instead if hook_exec(tmp_script, args=[app_instance_name], env=env_dict_remove) != 0: logger.warning( m18n.n('app_not_properly_removed', app=app_instance_name)) # Cleaning app directory shutil.rmtree(app_setting_path, ignore_errors=True) else: result['apps'].append(app_instance_name) finally: filesystem.rm(tmp_script, force=True) # Check if something has been restored if not result['hooks'] and not result['apps']: _clean_tmp_dir(1) raise MoulinetteError(errno.EINVAL, m18n.n('restore_nothings_done')) if result['apps']: app_ssowatconf(auth) _clean_tmp_dir() logger.success(m18n.n('restore_complete')) return result
def tools_postinstall(domain, password, ignore_dyndns=False): """ YunoHost post-install Keyword argument: domain -- YunoHost main domain ignore_dyndns -- Do not subscribe domain to a DynDNS service password -- YunoHost admin password """ from moulinette.core import init_authenticator from yunohost.app import app_ssowatconf from yunohost.firewall import firewall_upnp, firewall_reload dyndns = not ignore_dyndns try: with open('/etc/yunohost/installed') as f: pass except IOError: msignals.display(m18n.n('yunohost_installing')) else: raise MoulinetteError(errno.EPERM, m18n.n('yunohost_already_installed')) if len(domain.split('.')) >= 3 and not ignore_dyndns: try: r = requests.get('https://dyndns.yunohost.org/domains') except ConnectionError: pass else: dyndomains = json.loads(r.text) dyndomain = '.'.join(domain.split('.')[1:]) if dyndomain in dyndomains: if requests.get('https://dyndns.yunohost.org/test/%s' % domain).status_code == 200: dyndns=True else: raise MoulinetteError(errno.EEXIST, m18n.n('dyndns_unavailable')) # Create required folders folders_to_create = [ '/etc/yunohost/apps', '/etc/yunohost/certs', '/var/cache/yunohost/repo', '/home/yunohost.backup', '/home/yunohost.app' ] for folder in folders_to_create: try: os.listdir(folder) except OSError: os.makedirs(folder) # Change folders permissions os.system('chmod 755 /home/yunohost.app') # Set hostname to avoid amavis bug if os.system('hostname -d') != 0: os.system('hostname yunohost.yunohost.org') # Add a temporary SSOwat rule to redirect SSO to admin page try: with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) except IOError: ssowat_conf = {} if 'redirected_urls' not in ssowat_conf: ssowat_conf['redirected_urls'] = {} ssowat_conf['redirected_urls']['/'] = domain +'/yunohost/admin' with open('/etc/ssowat/conf.json.persistent', 'w+') as f: json.dump(ssowat_conf, f, sort_keys=True, indent=4) os.system('chmod 644 /etc/ssowat/conf.json.persistent') # Create SSL CA ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' command_list = [ 'echo "01" > %s/serial' % ssl_dir, 'rm %s/index.txt' % ssl_dir, 'touch %s/index.txt' % ssl_dir, 'cp %s/openssl.cnf %s/openssl.ca.cnf' % (ssl_dir, ssl_dir), 'sed -i "s/yunohost.org/%s/g" %s/openssl.ca.cnf ' % (domain, ssl_dir), 'openssl req -x509 -new -config %s/openssl.ca.cnf -days 3650 -out %s/ca/cacert.pem -keyout %s/ca/cakey.pem -nodes -batch' % (ssl_dir, ssl_dir, ssl_dir), 'cp %s/ca/cacert.pem /etc/ssl/certs/ca-yunohost_crt.pem' % ssl_dir, 'update-ca-certificates' ] for command in command_list: if os.system(command) != 0: raise MoulinetteError(errno.EPERM, m18n.n('yunohost_ca_creation_failed')) # Instantiate LDAP Authenticator auth = init_authenticator(('ldap', 'default'), { 'uri': "ldap://localhost:389", 'base_dn': "dc=yunohost,dc=org", 'user_rdn': "cn=admin" }) auth.authenticate('yunohost') # Initialize YunoHost LDAP base tools_ldapinit(auth) # New domain config tools_maindomain(auth, old_domain='yunohost.org', new_domain=domain, dyndns=dyndns) # Generate SSOwat configuration file app_ssowatconf(auth) # Change LDAP admin password tools_adminpw(old_password='******', new_password=password) # Enable uPnP firewall_upnp(action=['enable']) try: firewall_reload() except MoulinetteError: firewall_upnp(action=['disable']) # Enable iptables at boot time os.system('update-rc.d yunohost-firewall defaults') os.system('touch /etc/yunohost/installed') msignals.display(m18n.n('yunohost_configured'), 'success')