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"