/* # Copyright (c) 2009 - OpenSLX Project, Computer Center University of Freiburg # # This program is free software distributed under the GPL version 2. # See http://openslx.org/COPYING # # If you have any feedback please consult http://openslx.org/feedback and # send your suggestions, praise, or complaints to feedback@openslx.org # # General information about OpenSLX can be found at http://openslx.org/ # -------------------------------------------------------------------------- # pvsCheckPrivileges.cpp: # - A small program that checks whether or not it is called from # a physical seat and conditionally executes the pvs input daemon. # Additional security-relevant conditions should be checked here. # # The program is called with exactly one parameter, specifying the # number of the file descriptor which is to be passed to its child. # -------------------------------------------------------------------------- */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "pvsPrivInputSocket.h" #include "pvsCheckPrivileges.h" using namespace std; #define TIMEOUT_VALUE 10000 /* Wait for max. 10 seconds */ // We need these classes for PolicyKit access: struct PolKitSubject { QString subject_kind; QMap subject_details; }; Q_DECLARE_METATYPE(PolKitSubject); QDBusArgument& operator<<(QDBusArgument& arg, PolKitSubject const& subj) { arg.beginStructure(); arg << subj.subject_kind << subj.subject_details; arg.endStructure(); return arg; } QDBusArgument const& operator>>(QDBusArgument const& arg, PolKitSubject& subj) { arg.beginStructure(); arg >> subj.subject_kind >> subj.subject_details; arg.endStructure(); return arg; } struct PolKitAuthReply { bool isAuthorized; bool isChallenge; QMap details; }; Q_DECLARE_METATYPE(PolKitAuthReply); QDBusArgument& operator<<(QDBusArgument& arg, PolKitAuthReply const& reply) { arg.beginStructure(); arg << reply.isAuthorized << reply.isChallenge << reply.details; arg.endStructure(); return arg; } QDBusArgument const& operator>>(QDBusArgument const& arg, PolKitAuthReply& reply) { arg.beginStructure(); arg >> reply.isAuthorized >> reply.isChallenge >> reply.details; arg.endStructure(); return arg; } // We need to pass QMap to QVariant: typedef QMap QStringStringMap; Q_DECLARE_METATYPE(QStringStringMap) PVSCheckPrivileges* PVSCheckPrivileges::instance() { static PVSCheckPrivileges* static_instance = 0; if(!static_instance) { static_instance = new PVSCheckPrivileges(); } return static_instance; } void PVSCheckPrivileges::deleteInstance() { delete instance(); } PVSCheckPrivileges::PVSCheckPrivileges(QObject* parent) : QObject(parent) { // initialise our cache: updateCachedUserDatabase(); rereadConfiguration(); // and make it update itself: QStringList paths; paths << "/etc/passwd" << "/etc/group" << pvsPrivInputGetSettingsPath(); _watcher = new QFileSystemWatcher(paths, this); connect(_watcher, SIGNAL(fileChanged(QString const&)), this, SLOT(updateCachedUserDatabase())); connect(_watcher, SIGNAL(fileChanged(QString const&)), this, SLOT(rereadConfiguration())); } PVSCheckPrivileges::~PVSCheckPrivileges() { } QString PVSCheckPrivileges::getSessionReference(CachedInputContext const& sender) { if(!sender.isValid()) { return QString(); } QString sessionReference = _savedConsoleKitSession.value(sender, QString()); if(sessionReference.isNull()) { QDBusConnection conn = QDBusConnection::systemBus(); // Find the name of the current session: QDBusInterface manager("org.freedesktop.ConsoleKit", "/org/freedesktop/ConsoleKit/Manager", "org.freedesktop.ConsoleKit.Manager", conn); QDBusReply replyGetSession = manager.call(QDBus::Block, "GetSessionForUnixProcess", (quint32)sender.pid); if(!replyGetSession.isValid()) { qWarning("Reply to GetSessionForUnixProcess is invalid: %s: %s", replyGetSession.error().name().toLocal8Bit().constData(), replyGetSession.error().message().toLocal8Bit().constData()); return QString(); } _savedConsoleKitSession[sender] = sessionReference = replyGetSession.value().path(); } return sessionReference; } PVSCheckPrivileges::SessionKind PVSCheckPrivileges::getSessionKind(CachedInputContext const& sender) { if(!sender.isValid()) { return SESSION_UNKNOWN; } // if the sender is root, we always suppose he works locally. if(sender.uid == 0) { return SESSION_LOCAL; } QString sessionReference = getSessionReference(sender); if(sessionReference.isNull()) { return SESSION_LOOKUP_FAILURE; } QDBusConnection conn = QDBusConnection::systemBus(); QDBusInterface session("org.freedesktop.ConsoleKit", sessionReference, "org.freedesktop.ConsoleKit.Session", conn); QDBusReply replyIsLocal = session.call(QDBus::Block, "IsLocal"); if(!replyIsLocal.isValid()) { qWarning("Unable to find out whether the current session is local: %s: %s", replyIsLocal.error().name().toLocal8Bit().constData(), replyIsLocal.error().message().toLocal8Bit().constData()); return SESSION_LOOKUP_FAILURE; } return replyIsLocal.value() ? SESSION_LOCAL : SESSION_NONLOCAL; } PVSCheckPrivileges::UserPrivilege PVSCheckPrivileges::getUserPrivilege(CachedInputContext const& sender) { // Always allow root: if(sender.uid == 0) { return USER_PRIVILEGED; } // Or if the user is one of those enumerated in the privileged-users configuration value: if(_privilegedUsers.contains(sender.uid)) { return USER_PRIVILEGED; } // Or if the user is a member of one of the groups enumerated in the privileged-groups configuration value: foreach(gid_t gid, _privilegedGroups) { if(_userGroupMap.contains(sender.uid, gid)) { return USER_PRIVILEGED; } } // User is not trivially privileged, so try to check with PolicyKit. #ifdef HAVE_POLKIT // but only if it is present // For PolKit, we need the start-time of the process. // On Linux, we can get it from /proc: QString procStat = QString("/proc/%1/stat").arg(sender.pid); QFile procStatFile(procStat); if(!procStatFile.exists()) { qWarning("Could not look up any info on process %d, its %s file does not exist", sender.pid, procStat.toLocal8Bit().constData()); return USER_LOOKUP_FAILURE; } procStatFile.open(QIODevice::ReadOnly); QByteArray procStatBytes = procStatFile.readAll(); qDebug() << "Read stat file: " << procStatBytes; QString procStatLine = QString::fromLocal8Bit(procStatBytes.constData(), procStatBytes.length()); QStringList procStatFields = procStatLine.split(QRegExp("\\s+")); qDebug() << "Found stat fields: " << procStatFields; bool ok; quint64 startTime = procStatFields[21].toULongLong(&ok); if(!ok) { qWarning("Could not find out start time for process %d", (int)sender.pid); return USER_LOOKUP_FAILURE; } // Okay, we got the startTime. Now ask PolicyKit: QDBusConnection conn = QDBusConnection::systemBus(); QDBusInterface intf("org.freedesktop.PolicyKit1", "/org/freedesktop/PolicyKit1/Authority", "org.freedesktop.PolicyKit1.Authority", conn); PolKitSubject subj; subj.subject_kind = "unix-process"; subj.subject_details["pid"] = (quint32)sender.pid; subj.subject_details["start-time"] = startTime; QDBusReply reply = intf.call(QDBus::Block, QLatin1String("CheckAuthorization"), QVariant::fromValue(subj), "org.openslx.pvs.privilegedinput", QVariant::fromValue(QMap()) /* No details */, (quint32)1 /* Allow Interaction */, QUuid::createUuid().toString() /* Cancellation ID */); if(!reply.isValid()) { QDBusError err = reply.error(); qWarning("Reply to CheckAuthorization is invalid: %s: %s", err.name().toLocal8Bit().constData(), err.message().toLocal8Bit().constData()); return USER_LOOKUP_FAILURE; } return reply.value().isAuthorized ? USER_PRIVILEGED : USER_UNPRIVILEGED; #else return USER_UNPRIVILEGED; #endif } QString PVSCheckPrivileges::getX11SessionName(CachedInputContext const& sender) { QString sessionReference = getSessionReference(sender); if(sessionReference.isNull()) { return QString(); } QDBusConnection conn = QDBusConnection::systemBus(); QDBusInterface intf("org.freedesktop.ConsoleKit", sessionReference, "org.freedesktop.ConsoleKit.Session", conn); QDBusReply reply = intf.call(QDBus::Block, QLatin1String("GetX11Display")); if(!reply.isValid()) { QDBusError err = reply.error(); qWarning("Reply to GetX11Display is invalid: %s: %s", err.name().toLocal8Bit().constData(), err.message().toLocal8Bit().constData()); return QString(); } return reply.value(); } QString PVSCheckPrivileges::getX11DisplayDevice(CachedInputContext const& sender) { QString sessionReference = getSessionReference(sender); if(sessionReference.isNull()) { return QString(); } QDBusConnection conn = QDBusConnection::systemBus(); QDBusInterface intf("org.freedesktop.ConsoleKit", sessionReference, "org.freedesktop.ConsoleKit.Session", conn); QDBusReply reply = intf.call(QDBus::Block, QLatin1String("GetX11DisplayDevice")); if(!reply.isValid()) { QDBusError err = reply.error(); qWarning("Reply to GetX11DisplayDevice is invalid: %s: %s", err.name().toLocal8Bit().constData(), err.message().toLocal8Bit().constData()); return QString(); } return reply.value(); } bool PVSCheckPrivileges::require(SessionKind sessionKind, CachedInputContext const& sender) { SessionKind cachedSessionKind; if(sessionKind == SESSION_NONLOCAL) { // All sessions are at least non-local return true; } else if(sessionKind == SESSION_LOCAL) { if((cachedSessionKind = _savedSessionKind.value(sender, SESSION_UNKNOWN)) == SESSION_UNKNOWN) { cachedSessionKind = getSessionKind(sender); if(cachedSessionKind != SESSION_LOOKUP_FAILURE) _savedSessionKind[sender] = cachedSessionKind; qDebug("Got session kind: %s", toString(cachedSessionKind).toLocal8Bit().constData()); } switch(cachedSessionKind) { case SESSION_LOOKUP_FAILURE: case SESSION_UNKNOWN: { // If we cannot find out the correct session kind, look up what we should do in // the configuration: QSettings* config = pvsPrivInputGetSettings(); QVariant assumeLocal = config->value("assume-session-local", false); if(!assumeLocal.canConvert(QVariant::Bool)) { qWarning("There is an assume-session-local setting, but cannot convert it to boolean"); return false; } return assumeLocal.toBool(); } case SESSION_LOCAL: return true; case SESSION_NONLOCAL: return false; default: qWarning("Internal error: Undefined session kind %d", (int)cachedSessionKind); return false; } } else { qWarning("Internal error: It does not make sense to require an unknown session or undefined session kind %d", (int)sessionKind); return false; } } bool PVSCheckPrivileges::require(UserPrivilege userPrivilege, CachedInputContext const& sender) { UserPrivilege cachedUserPrivilege; if(userPrivilege == USER_UNPRIVILEGED) { // All users are unprivileged return true; } else if(userPrivilege == USER_PRIVILEGED) { if((cachedUserPrivilege = _savedUserPrivilege.value(sender, USER_UNKNOWN)) == USER_UNKNOWN) { cachedUserPrivilege = getUserPrivilege(sender); if(cachedUserPrivilege != USER_LOOKUP_FAILURE) _savedUserPrivilege[sender] = cachedUserPrivilege; qDebug("Got user privilege: %s", toString(cachedUserPrivilege).toLocal8Bit().constData()); } switch(cachedUserPrivilege) { case USER_LOOKUP_FAILURE: case USER_UNKNOWN: { // If we cannot find out the correct user privilege level, look up what we should do in // the configuration: QSettings* config = pvsPrivInputGetSettings(); QVariant assumePrivileged = config->value("assume-user-privileged", false); if(!assumePrivileged.canConvert(QVariant::Bool)) { qWarning("There is an assume-session-local setting, but cannot convert it to boolean"); return false; } return assumePrivileged.toBool(); } case USER_PRIVILEGED: return true; case USER_UNPRIVILEGED: return false; default: qWarning("Internal error: Found undefined user privilege level %d", (int)cachedUserPrivilege); _savedUserPrivilege.remove(sender); return false; } } else { qWarning("Internal error: It does not make sense to require an unknown or undefined user privilege level %d", (int)userPrivilege); return false; } } bool PVSCheckPrivileges::require(SessionKind sessionKind, UserPrivilege userPrivilege, CachedInputContext const& sender) { if(!require(sessionKind, sender)) return false; if(!require(userPrivilege, sender)) return false; return true; } uint qHash(CachedInputContext const& p) { return qHash(qMakePair(p.pid, qMakePair(p.uid, p.gid))); } void PVSCheckPrivileges::updateCachedUserDatabase() { QHash userNames; _userGroupMap.clear(); // assemble a list of known users and their primary groups: setpwent(); // open the user database struct passwd* userrec; while((userrec = getpwent())) { userNames.insert(userrec->pw_name, userrec->pw_uid); _userGroupMap.insert(userrec->pw_uid, userrec->pw_gid); } endpwent(); // close the database // augment with secondary groups: setgrent(); // open the group database struct group* grouprec; while((grouprec = getgrent())) { char** membername = grouprec->gr_mem; while(*membername) { uid_t uid = userNames.value(*membername, (uid_t)-1); if(uid != (uid_t)-1) { _userGroupMap.insert(uid, grouprec->gr_gid); } membername++; } } endgrent(); // decisions may have changed, so clear the caches: _savedUserPrivilege.clear(); } void PVSCheckPrivileges::rereadConfiguration() { QSettings* settings = pvsPrivInputReopenSettings(); _privilegedGroups.clear(); QVariant groupList = settings->value("privileged-groups"); if(groupList.isValid()) { if(!groupList.canConvert(QVariant::StringList)) { qWarning("There is a privileged-groups setting, but it cannot be converted to a list of strings."); goto endGroupListScan; } QStringList groups = groupList.toStringList(); foreach(QString groupName, groups) { bool ok; gid_t gid = groupName.toUInt(&ok); if(ok) { _privilegedGroups.append(gid); } else { // lookup the name: QByteArray groupNameBytes = groupName.toLocal8Bit(); struct group* group = getgrnam(groupNameBytes.constData()); if(group) { _privilegedGroups.append(group->gr_gid); } else { qWarning("privileged-groups setting contains %s which is neither a numeric GID " "nor a valid group name. Skipping.", groupNameBytes.constData()); } } } } endGroupListScan: _privilegedUsers.clear(); QVariant userList = settings->value("privileged-users"); if(userList.isValid()) { if(!userList.canConvert(QVariant::StringList)) { qWarning("There is a privileged-users setting, but it cannot be converted to a list of strings."); goto endUserListScan; } QStringList users = userList.toStringList(); foreach(QString userName, users) { bool ok; uid_t uid = userName.toUInt(&ok); if(ok) { _privilegedUsers.append(uid); } else { // lookup the name: QByteArray userNameBytes = userName.toLocal8Bit(); struct passwd* user = getpwnam(userNameBytes.constData()); if(user) { _privilegedUsers.append(user->pw_uid); } else { qWarning("privileged-users setting contains %s which is neither a numeric UID " "nor a valid user name. Skipping.", userNameBytes.constData()); } } } } endUserListScan: return; }