Commit d9692b4c authored by Harald Sitter's avatar Harald Sitter 🐰

add smb user management support

this rejiggers the model a bit and splits out user mapping logic into a
usermanager. the usermanager loads all users and models their samba
state. to do this it uses samba's pbedit tool. since this is a database
editor tool actually it needs a kauth helper to carry out the lookups.
this allows modelling of whether a user is enabled in samba or not (an
actual GUI for this is not part of this commit)

in addition to looking up the state this adds a new page for the page
stack for when the current user is not enabled in samba. this is to
prevent users from setting up shares but then not being able to access
them (assuming guest access is not possible - as is the case by default
without a smb.conf enabling support for it)

this new page sports a simple password setting UI that then again turns
to the auth helper for help. the auth helper runs smbpasswd, also a
samba CLI tool, to set a password for the user

all of this is conditional on samba actually having been configured to
use a local pdb instance as authentication database. other options would
be ldap or some such and will likely never be supported because they'd
only be used in corporate/managed environments where the user at hand
wouldn't be able to manage users anyway

BUG: 334875
FIXED-IN: 20.12
parent 133d94e3
########### next target ###############
set(sambausershareplugin_PART_SRCS sambausershareplugin.cpp model.cpp)
set(sambausershareplugin_PART_SRCS
sambausershareplugin.cpp
model.cpp
usermanager.cpp
)
if(SAMBA_INSTALL)
list(APPEND sambausershareplugin_PART_SRCS sambainstaller.cpp)
......@@ -17,6 +21,7 @@ target_link_libraries(sambausershareplugin
KF5::KIOWidgets
Qt5::Qml
Qt5::QuickWidgets
KF5::Auth
KF5::Declarative
)
......@@ -26,3 +31,12 @@ endif()
install(TARGETS sambausershareplugin DESTINATION ${PLUGIN_INSTALL_DIR})
install(FILES sambausershareplugin.desktop DESTINATION ${SERVICES_INSTALL_DIR})
# kauth
kauth_install_actions(org.kde.filesharing.samba org.kde.filesharing.samba.actions)
add_executable(authhelper authhelper.cpp)
target_link_libraries(authhelper KF5::AuthCore KF5::ConfigCore KF5::I18n)
kauth_install_helper_files(authhelper org.kde.filesharing.samba root)
install(TARGETS authhelper DESTINATION ${KAUTH_HELPER_INSTALL_DIR})
/*
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
*/
#include "authhelper.h"
#include <QProcess>
ActionReply AuthHelper::isuserknown(const QVariantMap &args)
{
const auto username = args.value(QStringLiteral("username")).toString();
if (username.isEmpty()) {
return ActionReply::HelperErrorReply();
}
QProcess p;
p.setProgram(QStringLiteral("pdbedit"));
p.setArguments({ QStringLiteral("--debuglevel=0"), QStringLiteral("--user"), username });
p.start();
// Should be fairly quick: short timeout.
const int pdbeditTimeout = 4000; // milliseconds
p.waitForFinished(pdbeditTimeout);
if (p.exitStatus() != QProcess::NormalExit) {
return ActionReply::HelperErrorReply();
}
ActionReply reply;
reply.addData(QStringLiteral("exists"), p.exitCode() == 0);
return reply;
}
ActionReply AuthHelper::createuser(const QVariantMap &args)
{
const auto username = args.value(QStringLiteral("username")).toString();
const auto password = args.value(QStringLiteral("password")).toString();
if (username.isEmpty() || password.isEmpty()) {
return ActionReply::HelperErrorReply();
}
QProcess p;
p.setProgram(QStringLiteral("smbpasswd"));
p.setArguments({
QStringLiteral("-L"), /* local mode */
QStringLiteral("-s"), /* read from stdin */
QStringLiteral("-D"), QStringLiteral("0"), /* force-disable debug */
QStringLiteral("-a"), /* add user */
username });
p.start();
// despite being in silent mode we still need to write the password twice!
p.write((password + QStringLiteral("\n")).toUtf8());
p.write((password + QStringLiteral("\n")).toUtf8());
p.waitForBytesWritten();
p.closeWriteChannel();
p.waitForFinished();
if (p.exitStatus() != QProcess::NormalExit) {
auto reply = ActionReply::HelperErrorReply();
reply.setErrorDescription(QString::fromUtf8(p.readAllStandardError()));
return reply;
}
ActionReply reply;
reply.addData(QStringLiteral("created"), p.exitCode() == 0);
// stderr will generally contain info on what went wrong so forward it
// so the UI may display it
reply.addData(QStringLiteral("stderr"), p.readAllStandardError());
return reply;
}
KAUTH_HELPER_MAIN("org.kde.filesharing.samba", AuthHelper)
/*
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
*/
#ifndef AUTHHELPER_H
#define AUTHHELPER_H
#include <KAuth>
using namespace KAuth;
class AuthHelper: public QObject
{
Q_OBJECT
public Q_SLOTS:
ActionReply isuserknown(const QVariantMap &args);
ActionReply createuser(const QVariantMap &args);
};
#endif // AUTHHELPER_H
......@@ -13,10 +13,11 @@
#include <sys/stat.h>
#include "model.h"
#include "usermanager.h"
UserPermissionModel::UserPermissionModel(const KSambaShareData &shareData, QObject *parent)
UserPermissionModel::UserPermissionModel(const KSambaShareData &shareData, UserManager *userManager, QObject *parent)
: QAbstractTableModel(parent)
, m_userList(getUsersList())
, m_userManager(userManager)
, m_shareData(shareData)
, m_usersAcl()
{
......@@ -42,53 +43,13 @@ void UserPermissionModel::setupData()
}
}
QStringList UserPermissionModel::getUsersList()
{
uid_t defminuid = 1000;
uid_t defmaxuid = 65000;
QFile loginDefs(QStringLiteral("/etc/login.defs"));
if (loginDefs.open(QIODevice::ReadOnly | QIODevice::Text)) {
while (!loginDefs.atEnd()) {
const QString line = QString::fromLatin1(loginDefs.readLine());
{
const QRegularExpression expression(QStringLiteral("^\\s*UID_MIN\\s+(?<UID_MIN>\\d+)"));
const auto match = expression.match(line);
if (match.hasMatch()) {
defminuid = match.captured(u"UID_MIN").toUInt();
}
}
{
const QRegularExpression expression(QStringLiteral("^\\s*UID_MAX\\s+(?<UID_MAX>\\d+)"));
const auto match = expression.match(line);
if (match.hasMatch()) {
defmaxuid = match.captured(u"UID_MAX").toUInt();
}
}
}
}
QStringList userList;
userList.append(QStringLiteral("Everyone"));
const QStringList userNames = KUser::allUserNames();
for (const QString &username : userNames) {
if (username == QLatin1String("nobody")) {
continue;
}
KUser user(username);
const uid_t nativeId = user.userId().nativeId();
if (nativeId >= defminuid && nativeId <= defmaxuid) {
userList << username;
}
}
return userList;
}
int UserPermissionModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_userList.count();
return m_userManager->users().count();
}
int UserPermissionModel::columnCount(const QModelIndex &parent) const
......@@ -100,13 +61,13 @@ int UserPermissionModel::columnCount(const QModelIndex &parent) const
QVariant UserPermissionModel::data(const QModelIndex &index, int role) const
{
if ((role == Qt::DisplayRole) && (index.column() == ColumnUsername)) {
return QVariant(m_userList.at(index.row()));
return QVariant(m_userManager->users().at(index.row())->name());
}
if ((role == Qt::DisplayRole || role == Qt::EditRole) && (index.column() == ColumnAccess)) {
QMap<QString, QVariant>::ConstIterator itr;
for (itr = m_usersAcl.constBegin(); itr != m_usersAcl.constEnd(); ++itr) {
if (itr.key().endsWith(m_userList.at(index.row()))) {
if (itr.key().endsWith(m_userManager->users().at(index.row())->name())) {
return itr.value();
}
}
......@@ -137,14 +98,14 @@ bool UserPermissionModel::setData(const QModelIndex &index, const QVariant &valu
QString key;
QMap<QString, QVariant>::ConstIterator itr;
for (itr = m_usersAcl.constBegin(); itr != m_usersAcl.constEnd(); ++itr) {
if (itr.key().endsWith(m_userList.at(index.row()))) {
if (itr.key().endsWith(m_userManager->users().at(index.row())->name())) {
key = itr.key();
break;
}
}
if (key.isEmpty()) {
key = m_userList.at(index.row());
key = m_userManager->users().at(index.row())->name();
}
if (value.isNull()) {
......
/*
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
SPDX-FileCopyrightText: 2011 Rodrigo Belem <rclbelem@gmail.com>
SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
*/
#ifndef model_h
......@@ -11,6 +12,7 @@
#include <ksambasharedata.h>
class KSambaShareData;
class UserManager;
class UserPermissionModel : public QAbstractTableModel
{
......@@ -22,7 +24,7 @@ public:
};
Q_ENUM(Column)
explicit UserPermissionModel(const KSambaShareData &shareData, QObject *parent = nullptr);
explicit UserPermissionModel(const KSambaShareData &shareData, UserManager *userManager, QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
......@@ -35,12 +37,11 @@ public:
QString getAcl() const;
private:
const QStringList m_userList;
UserManager const *m_userManager = nullptr;
const KSambaShareData m_shareData;
QVariantMap m_usersAcl;
Q_INVOKABLE void setupData();
static QStringList getUsersList();
};
#endif
[Domain]
Icon=preferences-system-network-share-windows
URL=https://www.kde.org
Name=KIO Samba Sharing
[org.kde.filesharing.samba.isuserknown]
# Always allow, there is nothing being written from this nor can one read anything special.
Policy=yes
PolicyInactive=yes
Name=Is Samba User Known
Description=Checking if Samba user exists
[org.kde.filesharing.samba.createuser]
# TODO: this could be split into a sepearate action createcurrentuser which only
# requires auth_self BUT it first needs kauth to grow the ability to resolve the dbus UID of the unprivileged
# remote (the client)
Policy=auth_admin
PolicyInactive=auth_admin
Name=Creating New Samba User
Description=Creating new Samba user
/*
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
SPDX-FileCopyrightText: 2020 Carson Black <uhhadd@gmail.com>
SPDX-FileCopyrightText: 2020 Harld Sitter <sitter@kde.org>
*/
import QtQuick 2.6
import QtQuick.Dialogs 1.1
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.5 as QQC2
import org.kde.kirigami 2.8 as Kirigami
Kirigami.OverlaySheet {
id: passwordRoot
property string password
property bool busy: false
property alias errorMessage: persistentError.text
signal accepted()
header: Kirigami.Heading {
// FIXME make qml user name aware so we can be more contextually accurate and label it 'create user foo'
text: i18nc("@title", "Create User")
}
function openAndClear() {
verifyField.text = ""
passwordField.text = ""
passwordField.forceActiveFocus()
open()
}
function isAcceptable() {
return !passwordWarning.visible && verifyField.text && passwordField.text;
}
function maybeAccept() {
if (!isAcceptable()) {
return
}
passwordRoot.password = passwordField.text
accepted()
}
function handleKeyEvent(event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
// 🤮 https://bugreports.qt.io/browse/QTBUG-70934
event.accepted = true
maybeAccept()
} else if (event.key === Qt.Key_Escape) {
// Handle Esc manually, within the sheet we'll want it to close the sheet rater than let the event fall
// through to a higher level item (or worse yet QWidget).
event.accepted = true
close()
}
}
ColumnLayout {
id: mainColumn
spacing: Kirigami.Units.smallSpacing
Layout.preferredWidth: Kirigami.Units.gridUnit * 15
// We don't use a FormLayout here because layouting breaks at small widths.
ColumnLayout {
id: inputLayout
Layout.alignment: Qt.AlignHCenter
visible: !busy.running
Kirigami.PasswordField {
id: passwordField
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
placeholderText: i18nc("@label:textbox", "Password")
// Reset external error on any password change
onTextChanged: errorMessage = ""
// Don't use onAccepted it's no bueno. See handleKeyEvent
}
Kirigami.PasswordField {
id: verifyField
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
placeholderText: i18nc("@label:textbox", "Confirm password")
// Reset external error on any password change
onTextChanged: errorMessage = ""
// Don't use onAccepted it's no bueno. See handleKeyEvent
}
Kirigami.InlineMessage {
id: passwordWarning
Layout.fillWidth: true
type: Kirigami.MessageType.Error
text: i18nc("@label error message", "Passwords must match")
visible: passwordField.text != "" && verifyField.text != "" && passwordField.text != verifyField.text
Layout.alignment: Qt.AlignLeft
}
// This is a separate, second, message because otherwise we'd have to do a whole state conversion dance
// logic that hurts my eyes.
// This message is for problems in the backend that we need to tell the user about. It's different in
// that the text is mutable and not controlled by us.
Kirigami.InlineMessage {
id: persistentError
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
type: Kirigami.MessageType.Error
visible: text != ""
}
QQC2.Button {
id: passButton
text: i18nc("@action:button creates a new samba user", "Create User")
enabled: isAcceptable()
Layout.alignment: Qt.AlignLeft
onClicked: maybeAccept()
}
}
QQC2.BusyIndicator {
id: busyIndicator
Layout.fillWidth: true
Layout.fillHeight: true
visible: running
running: passwordRoot.busy
}
}
}
/*
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
*/
import QtQuick 2.12
import QtQuick.Controls 2.5 as QQC2
import QtQuick.Layouts 1.14
import org.kde.kirigami 2.12 as Kirigami
import org.kde.filesharing.samba 1.0 as Samba
Kirigami.ScrollablePage {
background: Item {} /* this page is inside a tabbox, we want its background, not a window/page background */
Keys.onPressed: {
// We need to explicitly handle some keys inside the sheet. Since the sheet is no FocusScope we will catch
// them here and feed them to the sheet instead.
if (!changePassword.sheetOpen) {
return
}
changePassword.handleKeyEvent(event)
}
ChangePassword {
// This is an overlay sheet, it requires a scrollable page to anchor on.
id: changePassword
function userCreated(userCreated)
{
enabled = true
changePassword.busy = false
if (userCreated) {
close()
stack.push(pendingStack.pop())
}
}
onAccepted: {
enabled = false
busy = true
Samba.UserManager.currentUser().addToSamba(password)
}
}
Connections {
// ChangePassword being a sheet it's being crap to use and can't even connect to nothing.
target: Samba.UserManager.currentUser()
onInSambaChanged: changePassword.userCreated(target.inSamba)
onAddToSambaError: changePassword.errorMessage = error
}
ColumnLayout {
QQC2.Label {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
textFormat: Text.RichText // for xi18n markup
text: xi18nc("@info", `
<para>
Samba uses a separate user database from the system one.
This requires you to set a separate Samba password for every user that you want to
be able to authenticate with.
</para>
<para>
Before you can access shares with your current user account you need to set a Samba password.
</para>`)
wrapMode: Text.Wrap
}
QQC2.Button {
text: i18nc("@action:button opens dialog to create new user", "Create My Samba User")
onClicked: changePassword.openAndClear()
}
QQC2.Label {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
textFormat: Text.RichText // for xi18n markup
text: xi18nc("@info", `
Additional user management and password management can be done using Samba's <command>smbpasswd</command>
command line utility`)
wrapMode: Text.Wrap
}
}
}
......@@ -18,7 +18,7 @@ QQC2.StackView {
property var pendingStack: []
initialItem: QQC2.BusyIndicator {
running: true
running: !Samba.Plugin.ready
onRunningChanged: {
if (running) {
......@@ -26,6 +26,9 @@ QQC2.StackView {
}
pendingStack.push("ACLPage.qml")
if (!Samba.UserManager.currentUser().inSamba) {
pendingStack.push("UserPage.qml")
}
if (!Samba.Plugin.isSambaInstalled()) {
// NB: Samba.Installer may be not set when built without installer support
if (Samba.Installer === undefined) {
......@@ -39,7 +42,4 @@ QQC2.StackView {
stack.push(pendingStack.pop())
}
}
// Currently plugin doesn't lazy load anything. This is going to change eventually.
Component.onCompleted: initialItem.running = false
}
......@@ -9,5 +9,7 @@
<file>MissingSambaPage.qml</file>
<file>InstallPage.qml</file>
<file>RebootPage.qml</file>
<file>UserPage.qml</file>
<file>ChangePassword.qml</file>
</qresource>
</RCC>
......@@ -17,6 +17,8 @@
#include <QMetaMethod>
#include <QVBoxLayout>
#include <KLocalizedString>
#include <QTimer>
#include <QPushButton>
#include <KMessageBox>
#include <KPluginFactory>
......@@ -27,6 +29,7 @@
#include <KIO/ApplicationLauncherJob>
#include "model.h"
#include "usermanager.h"
#ifdef SAMBA_INSTALL
#include "sambainstaller.h"
......@@ -144,6 +147,7 @@ private:
SambaUserSharePlugin::SambaUserSharePlugin(QObject *parent, const QList<QVariant> &args)
: KPropertiesDialogPlugin(qobject_cast<KPropertiesDialog *>(parent))
, m_url(properties->item().mostLocalUrl().toLocalFile())
, m_userManager(new UserManager(this))
{
Q_UNUSED(args)
......@@ -159,7 +163,10 @@ SambaUserSharePlugin::SambaUserSharePlugin(QObject *parent, const QList<QVariant
// TODO: this could be made to load delayed via invokemethod. we technically don't need to fully load
// the backing data in the ctor, only the qml view with busyindicator
m_context = new ShareContext(properties->item().mostLocalUrl(), this);
m_model = new UserPermissionModel(m_context->m_shareData, this);
// FIXME maybe the manager ought to be owned by the model
qmlRegisterSingletonInstance("org.kde.filesharing.samba", 1, 0, "UserManager", m_userManager);
qmlRegisterUncreatableType<User>("org.kde.filesharing.samba", 1, 0, "User", QStringLiteral("Only created by UserManager"));
m_model = new UserPermissionModel(m_context->m_shareData, m_userManager, this);
#ifdef SAMBA_INSTALL
auto installer = new SambaInstaller;
......@@ -188,7 +195,19 @@ SambaUserSharePlugin::SambaUserSharePlugin(QObject *parent, const QList<QVariant
const QUrl url(QStringLiteral("qrc:/org.kde.filesharing.samba/qml/main.qml"));
widget->setSource(url);
properties->addPage(page, i18nc("@title:tab", "Share"));
auto item = properties->addPage(page, i18nc("@title:tab", "Share"));
if (qEnvironmentVariableIsSet("TEST_FOCUS_SHARE")) {
QTimer::singleShot(100, [item, this] {
properties->setCurrentPage(item);
});
}
QTimer::singleShot(0, [this] {
connect(m_userManager, &UserManager::loaded, this, [this] {
setReady(true);
});
m_userManager->load();
});
}
bool SambaUserSharePlugin::isSambaInstalled()
......@@ -307,5 +326,15 @@ void SambaUserSharePlugin::reportRemove(KSambaShareData::UserShareError error)
i18nc("@info/title", "Failed to Remove Network Share"));
}
bool SambaUserSharePlugin::isReady() const
{
return m_ready;
}