Umbau auf Pure-FTPd mit PureDB Virtual Users und Passwort-Dialog

- Komplett auf Pure-FTPd umgestellt (weg von vsftpd)
- PureDB virtuelle Benutzer: jeder User sieht nur seine Freigaben
- Per-User Bind-Mounts mit Read-Only/Read-Write Durchsetzung
- Passwort-Dialog beim Anlegen neuer FTP-Benutzer
- Change-Password UI: Dropdown + Button zum Passwort ändern
- Setup-Kommando für automatische Pure-FTPd Einrichtung
- Anonymous-Checkbox entfernt (nur authentifizierte User)
- sudoers-Fix: $SUDO_USER statt $USER bei sudo-Ausführung
- openssl passwd statt pure-pw stdin für GUI-Kompatibilität

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-04 19:29:36 +02:00
parent 34a94f0d82
commit 326d8e97ac
6 changed files with 460 additions and 142 deletions
+108 -20
View File
@@ -5,7 +5,9 @@
#include <QFormLayout>
#include <QGroupBox>
#include <QInputDialog>
#include <QLabel>
#include <QMessageBox>
#include <QProcess>
#include <QScrollArea>
#include <QVBoxLayout>
@@ -37,6 +39,26 @@ static int indexForAcl(const QString &acl)
return 0;
}
// Check if a virtual FTP user exists
static bool ftpUserExists(const QString &username)
{
QProcess proc;
proc.start(QStringLiteral("dolphin-ftp-share"),
{QStringLiteral("userexists"), username});
proc.waitForFinished(5000);
return proc.exitCode() == 0;
}
// Create a virtual FTP user with password
static bool createFtpUser(const QString &username, const QString &password)
{
QProcess proc;
proc.start(QStringLiteral("dolphin-ftp-share"),
{QStringLiteral("passwd"), username, password});
proc.waitForFinished(5000);
return proc.exitCode() == 0;
}
// Get list of local users (UID >= 1000, excluding nobody)
static QStringList getLocalUsers()
{
@@ -89,13 +111,8 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
nameLayout->addRow(i18n("Name:"), m_nameEdit);
mainLayout->addLayout(nameLayout);
// Anonymous access
m_anonymousCheckBox = new QCheckBox(i18n("Allow Anonymous Access"));
mainLayout->addWidget(m_anonymousCheckBox);
// User permissions section
auto *usersGroup = new QGroupBox();
usersGroup->setFlat(true);
auto *usersGroup = new QGroupBox(i18n("User Permissions"));
auto *usersLayout = new QVBoxLayout(usersGroup);
// Scroll area for user list
@@ -123,6 +140,18 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
usersLayout->addWidget(scrollArea);
mainLayout->addWidget(usersGroup);
// Change password section
auto *pwGroup = new QGroupBox(i18n("FTP Password"));
auto *pwLayout = new QHBoxLayout(pwGroup);
m_pwUserCombo = new QComboBox();
m_pwUserCombo->addItems(users);
auto *pwButton = new QPushButton(i18n("Change Password..."));
pwLayout->addWidget(m_pwUserCombo);
pwLayout->addWidget(pwButton);
mainLayout->addWidget(pwGroup);
connect(pwButton, &QPushButton::clicked, this, &FtpSharePlugin::onChangePassword);
mainLayout->addStretch();
// Connect signals
@@ -130,8 +159,6 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
this, &FtpSharePlugin::onShareToggled);
connect(m_nameEdit, &QLineEdit::textChanged,
this, [this]() { setDirty(true); });
connect(m_anonymousCheckBox, &QCheckBox::toggled,
this, [this]() { setDirty(true); });
// Load existing share info
loadCurrentShare();
@@ -146,7 +173,6 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
void FtpSharePlugin::onShareToggled(bool checked)
{
m_nameEdit->setEnabled(checked);
m_anonymousCheckBox->setEnabled(checked);
m_usersWidget->setEnabled(checked);
setDirty(true);
}
@@ -167,12 +193,10 @@ void FtpSharePlugin::loadCurrentShare()
// Format:
// [sharename]
// path=/some/path
// anonymous=y
// user_acl=user1:rw,user2:ro
QString currentSection;
QString sharePath;
QString shareAcl;
QString anonymous;
const QStringList lines = output.split(QLatin1Char('\n'));
for (const QString &line : lines) {
@@ -185,13 +209,10 @@ void FtpSharePlugin::loadCurrentShare()
currentSection = line.mid(1, line.indexOf(QLatin1Char(']')) - 1);
sharePath.clear();
shareAcl.clear();
anonymous.clear();
} else if (line.startsWith(QLatin1String("path="))) {
sharePath = line.mid(5);
} else if (line.startsWith(QLatin1String("user_acl="))) {
shareAcl = line.mid(9);
} else if (line.startsWith(QLatin1String("anonymous="))) {
anonymous = line.mid(10);
}
}
@@ -204,7 +225,6 @@ void FtpSharePlugin::loadCurrentShare()
m_originalShareName = currentSection;
m_shareCheckBox->setChecked(true);
m_nameEdit->setText(currentSection);
m_anonymousCheckBox->setChecked(anonymous == QLatin1String("y"));
// Parse ACL: "user1:rw,user2:ro"
if (!shareAcl.isEmpty()) {
@@ -257,6 +277,41 @@ void FtpSharePlugin::applyChanges()
proc.waitForFinished(5000);
}
// Create FTP users for users that have permissions but no FTP account yet
for (const auto &up : m_userPerms) {
if (up.combo->currentIndex() == 0)
continue; // No permission set, skip
if (ftpUserExists(up.username))
continue; // Already has FTP account
// Ask for FTP password
bool ok = false;
const QString password = QInputDialog::getText(
m_page,
i18n("FTP Password"),
i18n("Set FTP password for user '%1':", up.username),
QLineEdit::Password,
QString(),
&ok);
if (!ok || password.isEmpty()) {
QMessageBox::warning(m_page,
i18n("FTP Share"),
i18n("No password set for '%1'. This user will be skipped.", up.username));
up.combo->setCurrentIndex(0);
continue;
}
if (!createFtpUser(up.username, password)) {
QMessageBox::warning(m_page,
i18n("FTP Share"),
i18n("Failed to create FTP user '%1'.", up.username));
up.combo->setCurrentIndex(0);
continue;
}
}
// Build ACL string
QStringList aclParts;
for (const auto &up : m_userPerms) {
@@ -267,14 +322,11 @@ void FtpSharePlugin::applyChanges()
}
const QString aclString = aclParts.join(QLatin1Char(','));
const QString anonymous = m_anonymousCheckBox->isChecked()
? QStringLiteral("y")
: QStringLiteral("n");
// dolphin-ftp-share add <name> <path> <anonymous> <acl>
// dolphin-ftp-share add <name> <path> <acl>
QProcess proc;
proc.start(QStringLiteral("dolphin-ftp-share"),
{QStringLiteral("add"), shareName, m_path, anonymous, aclString});
{QStringLiteral("add"), shareName, m_path, aclString});
proc.waitForFinished(5000);
if (proc.exitCode() == 0) {
@@ -283,4 +335,40 @@ void FtpSharePlugin::applyChanges()
}
}
void FtpSharePlugin::onChangePassword()
{
const QString username = m_pwUserCombo->currentText();
if (username.isEmpty())
return;
if (!ftpUserExists(username)) {
QMessageBox::information(m_page,
i18n("FTP Password"),
i18n("User '%1' has no FTP account yet.\nSet permissions for this user first.", username));
return;
}
bool ok = false;
const QString password = QInputDialog::getText(
m_page,
i18n("FTP Password"),
i18n("New FTP password for '%1':", username),
QLineEdit::Password,
QString(),
&ok);
if (!ok || password.isEmpty())
return;
if (createFtpUser(username, password)) {
QMessageBox::information(m_page,
i18n("FTP Password"),
i18n("Password for '%1' updated.", username));
} else {
QMessageBox::warning(m_page,
i18n("FTP Password"),
i18n("Failed to update password for '%1'.", username));
}
}
#include "ftpshareplugin.moc"
+3 -1
View File
@@ -5,6 +5,7 @@
#include <QCheckBox>
#include <QComboBox>
#include <QLineEdit>
#include <QPushButton>
#include <QWidget>
struct UserPermission {
@@ -22,6 +23,7 @@ public:
private:
void loadCurrentShare();
void onShareToggled(bool checked);
void onChangePassword();
QString m_path;
QString m_defaultShareName;
@@ -29,8 +31,8 @@ private:
QWidget *m_page = nullptr;
QCheckBox *m_shareCheckBox = nullptr;
QLineEdit *m_nameEdit = nullptr;
QCheckBox *m_anonymousCheckBox = nullptr;
QWidget *m_usersWidget = nullptr;
QComboBox *m_pwUserCombo = nullptr;
QVector<UserPermission> m_userPerms;