/*
# 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 <sys/types.h>
#include <pwd.h>
#include <grp.h>
#include <unistd.h>
#include <iostream>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include <QFileSystemWatcher>
#include <QSettings>
#include <QTimer>
#include <QtDBus/QDBusArgument>
#include <QtDBus/QDBusConnection>
#include <QtDBus/QDBusInterface>
#include <QtDBus/QDBusMetaType>
#include <QtDBus/QDBusReply>
#include <QtGlobal>
#include <QDebug>
#include <QUuid>
#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<QString, QVariant> 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<QString, QString> 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<QString, QString> to QVariant:
typedef QMap<QString, QString> 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<QDBusObjectPath> 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<bool> 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<PolKitAuthReply> reply = intf.call(QDBus::Block,
QLatin1String("CheckAuthorization"),
QVariant::fromValue(subj),
"org.openslx.pvs.privilegedinput",
QVariant::fromValue(QMap<QString, QString>()) /* 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<QString> 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<QString> 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<QString, uid_t> 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;
}