# -*- coding: utf-8 -*- # Copyright 2010-2016 Mir Calculate. http://www.calculate-linux.org # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os from os import path import sys import time from calculate.core.server.core_interfaces import MethodsInterface from calculate.desktop._cl_keys import getKey from calculate.lib.utils.mount import isMount, childMounts from calculate.lib.utils.files import (process, getRunCommands, STDOUT, getLoopFromPath, getMdRaidDevices, listDirectory, removeDir, rsync_files, RsyncOptions, makeDirectory, getProgPath, FilesError) from calculate.lib.utils.common import (mountEcryptfs, CommonError, isBootstrapDataOnly) from calculate.lib.cl_lang import setLocalTranslate, getLazyLocalTranslate _ = lambda x: x setLocalTranslate('cl_desktop3', sys.modules[__name__]) __ = getLazyLocalTranslate(_) import tarfile import tempfile import shutil class DesktopError(Exception): """Desktop Error""" class Desktop(MethodsInterface): """ Модуль для настройки пользовательского сеанса и выполнения принудительного выхода из X сессии пользователя """ def __init__(self): self.homeDir = "" self.clTempl = None self.clVars = None def createCryptDir(self, userName, uid, gid, userDir, recreateOnError=False): """ Создать шифрование домашней директории, или подключить существующую userName,uid,gid,userDir: параметры пользовательской учётной записи recreateOnError: пересоздать профиль при ошбиках (используется при доменной ученой записи, так пользователь при этом ничего не теряет - профиль на сервере) """ userPwd = getKey(userName) error = "" # проверить наличие пароля в ключах ядра if not userPwd or userPwd == "XXXXXXXX": raise DesktopError(_("User password not found")) ecryptfsPath = path.join('/home/.ecryptfs', userName) # если шифрование уже используется if path.exists(ecryptfsPath): for d in (".ecryptfs", ".Private"): source, target = path.join(ecryptfsPath, d), path.join(userDir, d) if not path.lexists(target): os.symlink(source, target) # попытаться подключить шифрованные данные try: if not mountEcryptfs(userName, userPwd, userDir): error = _("Failed to mount ecrypted data") except CommonError as e: error = (_("Failed to mount ecrypted data") + _(": ") + '":%s"' % str(e)) # если при подключении произошли ошибки if error: # заархивировать текущий профиль и удалить его if recreateOnError: self.printSUCCESS(_("Recovering encrypted data")) if self.getMountUserPaths(userDir): raise DesktopError(_("Failed to encrypt the directory")) for source in (userDir, ecryptfsPath): if path.exists(source): if listDirectory(source): target = source + ".bak" newtarget = target if path.exists(target): removeDir(target) os.rename(source, newtarget) else: os.rmdir(source) self.createUserDir(userName, uid, gid, userDir) # ошибка создания шифрования else: raise DesktopError(error) # если нет шифрованных данных if not path.exists(ecryptfsPath): tf = None remove_files = None try: # если профиль содержит только данные от бутстрапа core if isBootstrapDataOnly(userDir): if childMounts(userDir): raise DesktopError( _("Failed to create an encrypted user profile") + _(": ") + _("The home directory contains mount points")) # поместить данные во временный tarfile remove_files = [] tf = tempfile.TemporaryFile() with tarfile.open(fileobj=tf, mode='w:') as tarf: for fn in listDirectory(userDir, fullPath=False): full_fn = path.join(userDir, fn) if path.lexists(full_fn): tarf.add(full_fn, fn) remove_files.append(full_fn) tf.flush() tf.seek(0) # удалить эти данные for fn in remove_files: if not path.islink(fn) and path.isdir(fn): shutil.rmtree(fn) else: os.unlink(fn) # создать шифрованные данные e = process('/usr/bin/ecryptfs-setup-private', '-u', userName, '-b', '-l', userPwd, stderr=STDOUT) if e.failed(): raise DesktopError(e.read()) # если были данные от бутстрапа, то распаковать их if tf and remove_files: with tarfile.open(fileobj=tf, mode='r:') as tarf: tarf.extractall(userDir) except Exception as e: # в случае ошибки сохраняем архив (с данными bootstrap) # из памяти в файловую систему if tf: tf.seek(0) bakArchName = path.join(userDir, ".calculate.tar.bz2") with open(bakArchName, 'w') as f: f.write(tf.read()) raise DesktopError( "%s\n%s" % (str(e), _("Failed to create an encrypted user profile"))) finally: if tf: tf.close() return True def createUserDir(self, userName, uid, gid, userDir, mode=0o700): """ Create user directory with need uid and gid """ if not path.exists(userDir): os.makedirs(userDir) if mode: os.chmod(userDir, mode) os.chown(userDir, uid, gid) return True else: raise DesktopError(_("Path %s exists") % userDir) def umountUserRes(self, *umountPaths): """ Отключить пользовательские ресурсы """ if umountPaths and type(umountPaths[0]) == list: umountPaths = umountPaths[0] for umountPath in umountPaths: if not self.umountSleepPath(umountPath): return False return True def getMountUserPaths(self, homeDir=False): """ Found user resources """ if not homeDir: self.clVars.Get("ur_login") homeDir = self.clVars.Get("ur_home_path") if not homeDir: raise DesktopError(_("Failed to determine the home directory")) dirStart, dirEnd = path.split(homeDir) mountProfileDir = path.join(dirStart, ".%s" % dirEnd) mountRemoteProfileDir = path.join(dirStart, ".%s.remote" % dirEnd) with open("/proc/mounts") as f: return list(filter(lambda x: x.startswith(homeDir) or x.startswith(mountProfileDir) or x.startswith(mountRemoteProfileDir), map(lambda x: x.split(" ")[1], f))) def umountSleepPath(self, rpath): """ Отмонтировать указанный путь, а также отключить используемые в этом пути loop устройства и raid """ # check for mount umount_cmd = getProgPath('/bin/umount') fuser_cmd = getProgPath("/bin/fuser") loops = getLoopFromPath(rpath) if loops: setLoops = set(map(lambda x: x.partition('/dev/')[2], loops)) mdInfo = getMdRaidDevices() for k, v in mdInfo.items(): if setLoops & set(v): self.umountSleepPath('/dev/%s' % k) process('/sbin/mdadm', '--stop', '/dev/%s' % k).success() for loop in loops: self.umountSleepPath(loop) process('/sbin/losetup', '-d', loop).success() if isMount(rpath): for waittime in [0, 0.5, 1, 2]: time.sleep(waittime) process(umount_cmd, rpath).success() if not isMount(rpath): return True process(fuser_cmd, "-km", rpath).success() for waittime in [0.5, 0.5, 0.5, 0.5, 0.5, 0.5]: time.sleep(waittime) if not isMount(rpath): return True process(umount_cmd, "-l", rpath).success() else: if isMount(rpath): self.printERROR(_("Failed to unmount directory %s") % rpath) return False return True def setFastlogin(self, urLogin): """ Отметить пользователя, что для него может быть использовать "быстрой логин" """ fastlogin = self.clVars.Get('cl_desktop_fastlogin_path') if not path.exists(fastlogin): makeDirectory(fastlogin) fastlogin_user = path.join(fastlogin, urLogin) if not path.exists(fastlogin_user): try: open(fastlogin_user, 'w').close() return True except IOError: self.printWARNING(_("Failed to create the fastlogin mark file")) return False def userLogout(self, urLogin): # Используется завершение сессии, так loginctl прибивает # сессию без завершающих скриптов return self.userLogoutBySession(urLogin) def getElogindSessionId(self, urLogin): loginctl = getProgPath("/bin/loginctl") p = process(loginctl, "--no-legend") try: for line in p: cols = [x.strip() for x in line.split()] if len(cols) >= 3: sessionid, uid, username = cols[:3] if username == urLogin: return sessionid finally: p.close() return None def terminateUserSession(self, session_id): loginctl = getProgPath("/bin/loginctl") p = process(loginctl, "terminate-session", session_id) return p.success() def userLogoutByElogind(self, urLogin): elogin_session_id = self.getElogindSessionId(urLogin) if not elogin_session_id: raise DesktopError(_("Unable to detect user session id")) return self.terminateUserSession(elogin_session_id) def userLogoutBySession(self, urLogin): """ Выполнить logout пользователя через dbus """ display = self.clVars.Select('cl_desktop_online_display', where='cl_desktop_online_user', eq=urLogin, limit=1) session = self.clVars.Get('cl_desktop_xsession') if session == 'xfce': logoutCommand = "/usr/bin/qdbus org.xfce.SessionManager " \ "/org/xfce/SessionManager " \ "org.xfce.Session.Manager.Logout False False" elif session == 'kde': logoutCommand = "/usr/bin/kquitapp ksmserver" elif session == 'plasma': logoutCommand = "/usr/bin/qdbus org.kde.ksmserver /KSMServer " \ "org.kde.KSMServerInterface.logout 0 0 0" elif session in ('gnome', 'mate', 'cinnamon-session'): logoutCommand = "/usr/bin/qdbus org.gnome.SessionManager " \ "/org/gnome/SessionManager " \ "org.gnome.SessionManager.Logout 1" elif session == 'lxqt': logoutCommand = "/usr/bin/qdbus org.lxqt.session " \ "/LXQtSession org.lxqt.session.logout" else: raise DesktopError(_("Unable to detect the X session")) if process("su", urLogin, "-c", ("DISPLAY=:%s " % display) + logoutCommand).failed(): raise DesktopError(_("Unable to send the logout command")) return True def waitLogout(self, urLogin, waitTime, postWaitTime=20): """ Ожидать завершения пользовательского сеанса Args: urLogin: логин пользователя waitTime: время ожидания завершения сеанса """ session = self.clVars.Get('cl_desktop_xsession') if session == "plasma": uid = self.clVars.Get('ur_uid') if uid: uid = int(uid) for i in range(0, postWaitTime): if not list(filter(lambda x: "ksmserver" in x, getRunCommands(uid=uid))): break time.sleep(1) time.sleep(1) if list(filter(lambda x: "xdm/xdm\x00--logout" in x, getRunCommands())): for i in range(0, waitTime): if not list(filter(lambda x: "xdm/xdm\x00--logout" in x, getRunCommands())): return True time.sleep(1) else: raise DesktopError(_("Unable to wait for completion " "of the user logout")) for wait in range(0, postWaitTime): self.clVars.Invalidate('cl_desktop_online_data') if urLogin not in self.clVars.Get('cl_desktop_online_user'): return True time.sleep(1) else: return False def prepareFace(self, ur_home_path): """Подготовить каталог пользователя с шифрованием для работы с .face Для шифрованных профилей в корне домашней директории создается симлинк на .ecryptfs/.face. В зашифрованном профиле такой симлинк будет создаваться шаблонами Args: ur_home_path: домашняя директория пользователя Returns: True/False в зависимости от успешности """ if path.exists(ur_home_path): symlink_path = path.join(ur_home_path, '.face') if not path.lexists(symlink_path): os.symlink('.ecryptfs/.face', symlink_path) return True else: return False def syncSkel(self, ur_home_path, uid, gid): """ Скопировать содержимое /etc/skel в каталог пользователя """ try: rsync_files('/etc/skel', ur_home_path, opts=(RsyncOptions.Archive, RsyncOptions.Chown(uid, gid),)) except FilesError as e: raise DesktopError(str(e)) return True