File indexing completed on 2025-01-05 04:35:36

0001 // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0002 // SPDX-FileCopyrightText: 2020-2022 Harald Sitter <sitter@kde.org>
0003 
0004 #include "plugin.h"
0005 
0006 #include <QDebug>
0007 #include <QQmlApplicationEngine>
0008 #include <QQmlContext>
0009 #include <QQuickItem>
0010 #include <QQuickWidget>
0011 #include <QVBoxLayout>
0012 
0013 #include <KIO/SpecialJob>
0014 #include <KLocalizedContext>
0015 #include <KLocalizedString>
0016 #include <KPluginFactory>
0017 
0018 #include "aceobject.h"
0019 #include "debug.h"
0020 #include "model.h"
0021 
0022 K_PLUGIN_CLASS_WITH_JSON(SambaACL, "samba-acl.json")
0023 
0024 constexpr int getACEMagic = 0xAC;
0025 constexpr int setACEMagic = 0xACD;
0026 
0027 class Context : public QObject
0028 {
0029     Q_OBJECT
0030 public:
0031     using QObject::QObject;
0032 
0033     Q_PROPERTY(Model *aceModel MEMBER m_aceModel CONSTANT)
0034     Model *m_aceModel = new Model(this);
0035 
0036     Q_PROPERTY(QList<QVariantMap> types READ types CONSTANT)
0037     [[nodiscard]] Q_INVOKABLE QList<QVariantMap> types()
0038     {
0039         static QList<QVariantMap> ret;
0040         if (!ret.isEmpty()) {
0041             return ret;
0042         }
0043         const auto enumerable = QMetaEnum::fromType<ACEObject::Type>();
0044         for (int i = 0; i < enumerable.keyCount(); ++i) {
0045             const int value = enumerable.value(i);
0046             QVariantMap map;
0047             map[QStringLiteral("text")] = typeToString(static_cast<ACEObject::Type>(value));
0048             map[QStringLiteral("value")] = value;
0049             ret << map;
0050 
0051         }
0052         return ret;
0053     }
0054 
0055     [[nodiscard]] static QString typeToString(ACEObject::Type type)
0056     {
0057         switch (type) {
0058         case ACEObject::Type::Deny:
0059             return i18nc("@option:radio an entry denying permissions", "Deny");
0060         case ACEObject::Type::Allow:
0061             return i18nc("@option:radio an entry allowing permissions", "Allow");
0062         }
0063         // We only support deny and allow for now (and samba does to apparently)
0064         Q_UNREACHABLE();
0065         return i18nc("@option:radio an unknown permission entry type (doesn't really happen)", "Unknown");
0066     }
0067 
0068     [[nodiscard]] static QString inheritanceToString(ACEObject::Inheritance inheritance)
0069     {
0070         switch (inheritance) {
0071         case ACEObject::Inheritance::None:
0072             return i18nc("@option:radio permission applicability type", "This folder only");
0073         case ACEObject::Inheritance::FolderSubfoldersFiles:
0074             return i18nc("@option:radio permission applicability type", "This folder, subfolders and files");
0075         case ACEObject::Inheritance::FolderSubfolders:
0076             return i18nc("@option:radio permission applicability type", "This folder and subfolders");
0077         case ACEObject::Inheritance::FolderFiles:
0078             return i18nc("@option:radio permission applicability type", "This folder and files");
0079         case ACEObject::Inheritance::SubfoldersFiles:
0080             return i18nc("@option:radio permission applicability type", "Subfolders and files only");
0081         case ACEObject::Inheritance::Subfolders:
0082             return i18nc("@option:radio permission applicability type", "Subfolders only");
0083         case ACEObject::Inheritance::Files:
0084             return i18nc("@option:radio permission applicability type", "Files only");
0085         }
0086         Q_UNREACHABLE();
0087         return i18nc("@option:radio permission applicability type (doesn't really happen)", "Unknown");
0088     }
0089 
0090     Q_PROPERTY(QList<QVariantMap> inheritances READ inheritances CONSTANT)
0091     [[nodiscard]] Q_INVOKABLE QList<QVariantMap> inheritances()
0092     {
0093         static QList<QVariantMap> ret;
0094         if (!ret.isEmpty()) {
0095             return ret;
0096         }
0097         const auto enumerable = QMetaEnum::fromType<ACEObject::Inheritance>();
0098         for (int i = 0; i < enumerable.keyCount(); ++i) {
0099             const int value = enumerable.value(i);
0100             QVariantMap map;
0101             map[QStringLiteral("text")] = inheritanceToString(static_cast<ACEObject::Inheritance>(value));
0102             map[QStringLiteral("value")] = value;
0103             ret << map;
0104 
0105         }
0106         return ret;
0107     }
0108 
0109     Q_PROPERTY(QString owner MEMBER m_owner NOTIFY ownerChanged)
0110     Q_SIGNAL void ownerChanged();
0111     QString m_owner;
0112 
0113     Q_PROPERTY(QString group MEMBER m_group NOTIFY groupChanged)
0114     Q_SIGNAL void groupChanged();
0115     QString m_group;
0116 };
0117 
0118 static Context &context()
0119 {
0120     static Context s_context;
0121     return s_context;
0122 }
0123 
0124     // TODO maybe introduce a unix mode if Unix Group\ exists. only a quarter of the ace mask has meaning because it translates to rwx
0125 
0126     /*
0127      * POSIX ACL
0128      * rwx => (ALL ACTRL mask bits set) :: 00000000000111110000000111111111
0129      * rw- => 00000000000100100000000010101001 :: ACTRL_DS_CREATE_CHILD | ACTRL_DS_SELF | ACTRL_DS_WRITE_PROP | ACTRL_DS_LIST_OBJECT | ACTRL_FILE_READ | ACTRL_FILE_READ_PROP | ACTRL_FILE_EXECUTE | ACTRL_FILE_READ_ATTRIB | ACTRL_DIR_LIST | ACTRL_DIR_TRAVERSE
0130      * r-- => 00000000000100100000000010001001 :: ACTRL_DS_CREATE_CHILD | ACTRL_DS_SELF | ACTRL_DS_LIST_OBJECT | ACTRL_FILE_READ | ACTRL_FILE_READ_PROP | ACTRL_FILE_READ_ATTRIB | ACTRL_DIR_LIST
0131      *
0132      * POSIX ACL default:* entries are mapped in the flags if applicable (i.e. INHERIT_ONLY_ACE | CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE)
0133      */
0134 
0135 SambaACL::SambaACL(QObject *parent)
0136     : KPropertiesDialogPlugin(parent)
0137     , m_url(properties->url())
0138     , m_page(new QWidget(qobject_cast<KPropertiesDialog *>(parent)))
0139 {
0140     auto parts = m_url.path().split(QLatin1Char('/'));
0141     parts.removeAll(QLatin1String());
0142     if (!m_url.isValid() || parts.isEmpty()) {
0143         return; // neither root nor host have permissions, shares may
0144     }
0145 
0146     qmlRegisterType<Model>("org.kde.filesharing.samba.acl", 1, 0, "ACEModel");
0147 
0148     auto engine = new QQmlApplicationEngine(this);
0149     m_page->setAttribute(Qt::WA_TranslucentBackground);
0150     auto widget = new QQuickWidget(engine, m_page.get());
0151 
0152     auto i18nContext = new KLocalizedContext(widget->engine());
0153     i18nContext->setTranslationDomain(QStringLiteral(TRANSLATION_DOMAIN));
0154     widget->engine()->rootContext()->setContextObject(i18nContext);
0155 
0156     widget->setResizeMode(QQuickWidget::SizeRootObjectToView);
0157     widget->setFocusPolicy(Qt::StrongFocus);
0158     widget->setAttribute(Qt::WA_AlwaysStackOnTop, true);
0159     widget->quickWindow()->setColor(Qt::transparent);
0160     auto layout = new QVBoxLayout(m_page.get());
0161     layout->addWidget(widget);
0162 
0163     qmlRegisterSingletonInstance<Context>("org.kde.filesharing.samba.acl", 1, 0, "Context", &context());
0164     widget->rootContext()->setContextProperty(QStringLiteral("plugin"), this);
0165 
0166     const QUrl url(QStringLiteral("qrc:/org.kde.filesharing.samba.acl/qml/main.qml"));
0167 
0168     QObject::connect(
0169         engine,
0170         &QQmlApplicationEngine::objectCreated,
0171         this,
0172         [url](QObject *obj, const QUrl &objUrl) {
0173             if (!obj && url == objUrl) {
0174                 qFatal("qml error");
0175             }
0176         },
0177         Qt::QueuedConnection);
0178 
0179     widget->setSource(url);
0180 
0181     if (!widget->rootObject()) {
0182         qFatal("error");
0183     }
0184 
0185     (void)properties->addPage(m_page.get(), i18nc("@title:tab", "Remote Permissions"));
0186     if (auto parentWidget = qobject_cast<QWidget *>(m_page->parent()); parentWidget && parentWidget->layout()) {
0187         // Force our encompassing layout to have no margins, our QML Controls have some already!
0188         parentWidget->layout()->setContentsMargins(0,0,0,0);
0189     }
0190 
0191     // TODO make this more discriminatory
0192     setDirty(true);
0193 
0194     refresh();
0195 }
0196 
0197 void SambaACL::applyChanges()
0198 {
0199     const auto acl = context().m_aceModel->acl();
0200     for (const auto &ace : acl) {
0201         if (ace->flags & INHERITED_ACE) { // cannot possibly be modified
0202             continue;
0203         }
0204         if (ace->originalXattr == ace->toSMBXattr()) { // unchanged
0205             continue;
0206         }
0207 
0208         qWarning() << "APPLYING CHANGES for!" << ace->sid;
0209         QByteArray packedArgs;
0210         QDataStream stream(&packedArgs, QIODevice::WriteOnly);
0211         stream << setACEMagic << m_url << ace->sid << ace->toSMBXattr();
0212 
0213         // TODO could start multiple setters maybe then wait for all of them
0214         auto job = KIO::special(m_url, packedArgs);
0215         job->exec();
0216     }
0217 }
0218 
0219 void SambaACL::refresh()
0220 {
0221     QByteArray packedArgs;
0222     QDataStream stream(&packedArgs, QIODevice::WriteOnly);
0223     stream << getACEMagic << m_url;
0224 
0225     auto job = KIO::special(m_url, packedArgs);
0226     connect(job, &KJob::finished, this, [this, job] {
0227         const QString aclString = job->metaData().value(QStringLiteral("ACL"));
0228         context().setProperty("owner", job->metaData().value(QStringLiteral("OWNER")));
0229         context().setProperty("group", job->metaData().value(QStringLiteral("GROUP")));
0230 
0231         const auto aceStrings = aclString.split(QLatin1Char(','));
0232         QList<std::shared_ptr<ACE>> acl;
0233         QRegularExpression r(QStringLiteral("(?<SID>.+):(?<TYPE>\\d+)/(?<FLAGS>\\d+)/(?<MASK>0[xX][0-9a-fA-F]+)"));
0234         for (const auto &aceString : aceStrings) {
0235             const auto match = r.match(aceString);
0236             qDebug() << match << aceString;
0237             if (!match.isValid() || !match.hasMatch()) {
0238                 continue;
0239             }
0240 
0241             std::shared_ptr<ACE> ace(new ACE{match.captured(QStringLiteral("SID")),
0242                                              (uint8_t)match.captured(QStringLiteral("TYPE")).toUShort(),
0243                                              (uint8_t)match.captured(QStringLiteral("FLAGS")).toUShort(),
0244                                              match.captured(QStringLiteral("MASK")).toUInt(nullptr, 16)});
0245 
0246             if (qEnvironmentVariableIntValue("KIO_SMB_ACL_DEBUG") > 1) {
0247                 printACE(*ace);
0248             }
0249             acl << ace;
0250         }
0251 
0252         context().m_aceModel->resetData(acl);
0253 
0254         m_ready = true;
0255         Q_EMIT readyChanged();
0256     });
0257     job->start();
0258 }
0259 
0260 #include "plugin.moc"