summaryrefslogblamecommitdiffstats
path: root/src/input/pvsCheckPrivileges.cpp
blob: 2026c0a9907e81bf5128aa1ab462dbabd9133974 (plain) (tree)




















                                                                               


                      



                   
                  


                           

                             








                                 
                               

























































                                                                                 










                                                           

 

                                                       
 









                                                                                                       
































































                                                                                                                                                                                                                






                                                                                                 
                                       









                                                                                                                   
 
                                                                            
                                               













































                                                                                                                                                          


                                 

















































                                                                                                                                                           





                                                      







                                                                                                              



















                                                                                                                       
                                     








                                                                                                                                                
         





                                                                                               





                                                 







                                                                                                                  























                                                                                                                       
                                     





                                                                                                                                                  
         
















                                                                


























































































































                                                                                                                           
/*
 # 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;
}