From 83faf632158047b57f7a4f6e5a7b0607a858ea53 Mon Sep 17 00:00:00 2001 From: yeshanshan Date: Sat, 25 Jul 2026 10:47:47 +0800 Subject: [PATCH] feat: add private permission management for dconfig - Add AppIdResolver class to resolve caller appId via AM Identify + /proc fallback - Add hasPermissionByVisibility() to enforce private config access control - Add getCallerAppId() for standard appId resolution (separate from getAppid() which returns process path) - Add parseConfigAppId() to extract config owner appId from file path - Insert visibility checks in value(), setValue(), reset(), isDefaultValue() - Return AccessDenied DBus error when non-owner appId accesses private config - Use mutable members to avoid const_cast in const methods - Use RAII for libdbus-1 resource management in identifyByAM() - Connect to user session bus via dbus_connection_open_private for cross-bus AM access PMS: BUG-000000 --- .../dde-dconfig-daemon/appidresolver.cpp | 187 ++++++++++++++++++ .../dde-dconfig-daemon/appidresolver.h | 36 ++++ .../dde-dconfig-daemon/dconfigconn.cpp | 92 ++++++++- .../dde-dconfig-daemon/dconfigconn.h | 18 +- .../dde-dconfig-daemon/dconfigserver.cpp | 42 +++- .../dde-dconfig-daemon/dconfigserver.h | 10 + dconfig-center/dde-dconfig-daemon/src.cmake | 2 + 7 files changed, 382 insertions(+), 5 deletions(-) create mode 100644 dconfig-center/dde-dconfig-daemon/appidresolver.cpp create mode 100644 dconfig-center/dde-dconfig-daemon/appidresolver.h diff --git a/dconfig-center/dde-dconfig-daemon/appidresolver.cpp b/dconfig-center/dde-dconfig-daemon/appidresolver.cpp new file mode 100644 index 0000000..ccab79f --- /dev/null +++ b/dconfig-center/dde-dconfig-daemon/appidresolver.cpp @@ -0,0 +1,187 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "appidresolver.h" + +#include +#include +#include +#include +#include + +#ifdef Q_OS_LINUX +#include +#include +#include +#include +#include +#endif + +Q_LOGGING_CATEGORY(cfLog, "dsg.config", QtInfoMsg); + +AppIdResolver::AppIdResolver(QObject *parent) + : QObject(parent) +{ +} + +QString AppIdResolver::resolveAppId(const ConnServiceName &service, uint pid, uint uid) +{ + // 检查缓存 + if (m_cache.contains(service)) { + return m_cache.value(service); + } + + QString appId; + +#ifdef Q_OS_LINUX + // 首先尝试通过 AM Identify 获取 + appId = identifyByAM(pid, uid); + + // 如果 AM 不可用,fallback 到 /proc 解析 + if (appId.isEmpty()) { + appId = fallbackFromProc(pid); + } +#else + // 非 Linux 平台直接 fallback + appId = fallbackFromProc(pid); +#endif + + // 缓存结果 + if (!appId.isEmpty()) { + m_cache.insert(service, appId); + } + + return appId; +} + +void AppIdResolver::clearCache(const ConnServiceName &service) +{ + m_cache.remove(service); +} + +#ifdef Q_OS_LINUX +QString AppIdResolver::identifyByAM(uint pid, uint uid) +{ + // RAII helpers for libdbus-1 resources + auto dbusErrorDeleter = [](DBusError *err) { if (err) { dbus_error_free(err); delete err; } }; + auto dbusConnDeleter = [](DBusConnection *c) { if (c) dbus_connection_unref(c); }; + auto dbusMsgDeleter = [](DBusMessage *m) { if (m) dbus_message_unref(m); }; + + // 构造 session bus 地址(daemon 运行在 system bus,需跨总线连接用户 session bus) + QByteArray address = QString("unix:path=/run/user/%1/bus").arg(uid).toUtf8(); + + auto error = std::unique_ptr(new DBusError, dbusErrorDeleter); + dbus_error_init(error.get()); + + // 使用 dbus_connection_open_private 连接到用户的 session bus + // dbus_bus_get(DBUS_BUS_SESSION) 不可用,因为 daemon 没有自己的 session bus + DBusConnection *rawConn = dbus_connection_open_private(address.constData(), error.get()); + if (!rawConn) { + qCDebug(cfLog) << "Failed to connect to session bus for uid" << uid << ":" << error->message; + return QString(); + } + auto connGuard = std::unique_ptr(rawConn, dbusConnDeleter); + + // 注册连接(完成认证握手) + if (!dbus_bus_register(rawConn, error.get())) { + qCDebug(cfLog) << "Failed to register on session bus for uid" << uid << ":" << error->message; + return QString(); + } + + // 获取 pidfd + int pidfd = syscall(SYS_pidfd_open, pid, 0); + if (pidfd < 0) { + qCDebug(cfLog) << "Failed to get pidfd for pid" << pid << ":" << strerror(errno); + return QString(); + } + + // 创建 Identify 方法调用 + DBusMessage *rawMsg = dbus_message_new_method_call( + "org.desktopspec.ApplicationManager1", + "/org/desktopspec/ApplicationManager1", + "org.desktopspec.ApplicationManager1", + "Identify"); + if (!rawMsg) { + qCDebug(cfLog) << "Failed to create D-Bus message for Identify"; + close(pidfd); + return QString(); + } + auto msgGuard = std::unique_ptr(rawMsg, dbusMsgDeleter); + + // 附加 pidfd 参数 + if (!dbus_message_append_args(rawMsg, DBUS_TYPE_UNIX_FD, &pidfd, DBUS_TYPE_INVALID)) { + qCDebug(cfLog) << "Failed to append pidfd to D-Bus message"; + close(pidfd); + return QString(); + } + + // 发送调用并等待回复 + DBusMessage *rawReply = dbus_connection_send_with_reply_and_block(rawConn, rawMsg, 5000, error.get()); + msgGuard.reset(); // msg consumed by the call + close(pidfd); + + if (dbus_error_is_set(error.get())) { + qCDebug(cfLog) << "Identify call failed:" << error->message; + return QString(); + } + + if (!rawReply) { + qCDebug(cfLog) << "No reply received from Identify"; + return QString(); + } + auto replyGuard = std::unique_ptr(rawReply, dbusMsgDeleter); + + // 解析回复 + const char *appIdStr = nullptr; + if (!dbus_message_get_args(rawReply, error.get(), DBUS_TYPE_STRING, &appIdStr, DBUS_TYPE_INVALID)) { + qCDebug(cfLog) << "Failed to parse Identify reply:" << error->message; + return QString(); + } + + QString result = QString::fromUtf8(appIdStr); + qCDebug(cfLog) << "Got appId from AM:" << result << "for pid" << pid; + return result; +} +#endif + +QString AppIdResolver::fallbackFromProc(uint pid) +{ + // 从 /proc/{pid}/exe 获取进程路径 + QString exePath = QFile::symLinkTarget(QString("/proc/%1/exe").arg(pid)); + + if (exePath.isEmpty()) { + // Fallback to cmdline + QFile cmdlineFile(QString("/proc/%1/cmdline").arg(pid)); + if (cmdlineFile.open(QIODevice::ReadOnly)) { + QByteArray cmdline = cmdlineFile.readLine(); + exePath = cmdline.split('\0').first(); + } + } + + if (exePath.isEmpty()) { + return QString(); + } + + // 格式化为 appId + // 参考 DSGApplication::formatAppId 的实现 + QString appId = exePath; + + // 移除前导路径分隔符并替换为 . + appId = appId.replace(QRegularExpression(QStringLiteral("^/+")), QString()); + appId = appId.replace(QDir::separator(), QStringLiteral(".")); + + // 替换特殊字符为 - + static const QRegularExpression regex(QStringLiteral("[^\\w\\-\\.]")); + appId = appId.replace(regex, QStringLiteral("-")); + + // 移除开头的 . + static const QString dotPrefix = QStringLiteral("."); + while (appId.startsWith(dotPrefix)) { + appId = appId.mid(dotPrefix.size()); + } + + qCDebug(cfLog) << "Fallback appId from /proc:" << appId << "for pid" << pid; + + return appId; +} diff --git a/dconfig-center/dde-dconfig-daemon/appidresolver.h b/dconfig-center/dde-dconfig-daemon/appidresolver.h new file mode 100644 index 0000000..0ef8c6a --- /dev/null +++ b/dconfig-center/dde-dconfig-daemon/appidresolver.h @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include "dconfig_global.h" +#include +#include +#include + +class AppIdResolver : public QObject { + Q_OBJECT +public: + explicit AppIdResolver(QObject *parent = nullptr); + ~AppIdResolver() override = default; + + // 主入口:通过 service/pid/uid 解析标准 appId + // service: DBus service name,用于缓存 key + // pid: 调用方进程 ID + // uid: 调用方用户 ID + QString resolveAppId(const ConnServiceName &service, uint pid, uint uid); + + // 清理指定 service 的缓存 + void clearCache(const ConnServiceName &service); + +private: + // 通过 AM Identify 获取 appId + QString identifyByAM(uint pid, uint uid); + + // 从 /proc/exe 格式化 appId 作为 fallback + QString fallbackFromProc(uint pid); + + // service -> appId 缓存 + QHash m_cache; +}; diff --git a/dconfig-center/dde-dconfig-daemon/dconfigconn.cpp b/dconfig-center/dde-dconfig-daemon/dconfigconn.cpp index bacf7df..16662a3 100644 --- a/dconfig-center/dde-dconfig-daemon/dconfigconn.cpp +++ b/dconfig-center/dde-dconfig-daemon/dconfigconn.cpp @@ -46,12 +46,24 @@ void DSGConfigConn::setResource(DSGConfigResource *resource) m_resource = resource; } +void DSGConfigConn::setConfigAppId(const QString &appId) +{ + m_configAppId = appId; +} + +void DSGConfigConn::setAppIdResolver(AppIdResolver *resolver) +{ + m_resolver = resolver; +} + /*! \brief 返回配置内容的所有配置项 \return */ QStringList DSGConfigConn::keyList() const { + // keyList 是元信息,但为避免泄露 private key 名称,需要检查权限 + // 暂时保持开放,因为客户端需要知道有哪些 key return meta()->keyList(); } @@ -117,6 +129,9 @@ void DSGConfigConn::setValue(const QString &key, const QDBusVariant &value) if (!hasPermissionByUid(key)) return; + + if (!hasPermissionByVisibility(key)) + return; const auto &v = decodeQDBusArgument(value.variant()); qCDebug(cfLog) << "Set value, key:" << key << ", now value:" << v << ", old value:" << file()->value(key, cache()); @@ -135,6 +150,9 @@ void DSGConfigConn::reset(const QString &key) if (!contains(key)) return; + if (!hasPermissionByVisibility(key)) + return; + qCDebug(cfLog) << "Reset value, key:" << key << ", old value:" << file()->value(key, cache()); if(!file()->setValue(key, QVariant(), getAppid(), cache())) return; @@ -158,6 +176,9 @@ QDBusVariant DSGConfigConn::value(const QString &key) if (!hasPermissionByUid(key)) return QDBusVariant(); + + if (!hasPermissionByVisibility(key)) + return QDBusVariant(); // Try to get value from cache. auto value = file()->cacheValue(cache(), key); @@ -203,6 +224,9 @@ bool DSGConfigConn::isDefaultValue(const QString &key) { if (!contains(key)) return false; + + if (!hasPermissionByVisibility(key)) + return false; // Try to get value from cache. auto value = file()->cacheValue(cache(), key); @@ -238,19 +262,47 @@ int DSGConfigConn::flags(const QString &key) return static_cast(meta()->flags(key)); } +/*! + \brief 获取调用方的进程路径(用于日志和 setValue 记录) + \return 进程可执行文件路径 + */ QString DSGConfigConn::getAppid() const { if (calledFromDBus()) { const QString &service = message().service(); if (m_lastService != service) { - const_cast(this)->m_lastService = service; - const_cast(this)->m_appName = getProcessNameByPid(connection().interface()->servicePid(service)); + m_lastService = service; + m_appName = getProcessNameByPid(connection().interface()->servicePid(service)); } return m_appName; } return QString("testappid"); } +/*! + \brief 获取调用方的标准 appId(用于 private 权限校验) + \return 标准 appId,如 org.deepin.dde.control-center + */ +QString DSGConfigConn::getCallerAppId() const +{ + if (!calledFromDBus()) + return QString(); + + if (!m_resolver) { + qCWarning(cfLog) << "AppIdResolver is not set, cannot get caller appId"; + return QString(); + } + + const QString &service = message().service(); + if (m_lastAppIdService != service) { + m_lastAppIdService = service; + uint pid = connection().interface()->servicePid(service); + uint uid = connection().interface()->serviceUid(service); + m_callerAppId = m_resolver->resolveAppId(service, pid, uid); + } + return m_callerAppId; +} + bool DSGConfigConn::contains(const QString &key) { if (containsWithoutProp(key)) @@ -301,3 +353,39 @@ bool DSGConfigConn::hasPermissionByUid(const QString &key) const } return hasPermission; } + +/*! + \brief 检查 private 权限 + 当配置项 visibility 为 private 时,仅允许配置所属 appId 的应用访问 + \a key 配置项名称 + \return 是否有权限 + */ +bool DSGConfigConn::hasPermissionByVisibility(const QString &key) const +{ + if (!calledFromDBus()) + return true; + + // public 配置允许所有应用访问 + if (meta()->visibility(key) == DConfigFile::Public) + return true; + + // private 配置:检查调用方 appId 是否匹配配置的 appId + const QString &callerAppId = getCallerAppId(); + const QString &configAppId = m_configAppId; + + // generic 配置(无 appId)允许访问 + if (configAppId.isEmpty() || configAppId == VirtualInterAppId) + return true; + + // appId 匹配则允许 + if (callerAppId == configAppId) + return true; + + // 拒绝访问 + QString errorMsg = QString("[%1] No permission to access private config item [%2] in [%3], " + "owner appId is [%4].") + .arg(callerAppId).arg(key).arg(m_key).arg(configAppId); + sendErrorReply(QDBusError::AccessDenied, errorMsg); + qWarning() << qPrintable(errorMsg); + return false; +} diff --git a/dconfig-center/dde-dconfig-daemon/dconfigconn.h b/dconfig-center/dde-dconfig-daemon/dconfigconn.h index 67d12a6..1ad2ab0 100644 --- a/dconfig-center/dde-dconfig-daemon/dconfigconn.h +++ b/dconfig-center/dde-dconfig-daemon/dconfigconn.h @@ -5,6 +5,7 @@ #pragma once #include "dconfig_global.h" +#include "appidresolver.h" #include #include #include @@ -35,6 +36,13 @@ class DSGConfigConn : public QObject, protected QDBusContext bool containsWithoutProp(const QString &key) const; void setResource(DSGConfigResource *resource); + + // 设置配置文件所属的 appId + void setConfigAppId(const QString &appId); + + // 设置 appId 解析器 + void setAppIdResolver(AppIdResolver *resolver); + Q_SIGNALS: void releaseChanged(const ConnServiceName &service); @@ -62,15 +70,21 @@ public Q_SLOTS: // METHODS private: QString getAppid() const; + QString getCallerAppId() const; // 获取调用方的标准 appId(用于权限检查) bool contains(const QString &key); DTK_CORE_NAMESPACE::DConfigMeta *meta() const; DTK_CORE_NAMESPACE::DConfigFile *file() const; DTK_CORE_NAMESPACE::DConfigCache *cache() const; bool hasPermissionByUid(const QString &key) const; + bool hasPermissionByVisibility(const QString &key) const; // 检查 private 权限 private: ConnKey m_key; DSGConfigResource *m_resource = nullptr; - QString m_appName; - QString m_lastService; + QString m_configAppId; // 配置文件所属的 appId + mutable QString m_callerAppId; // 调用方的标准 appId(用于权限检查) + mutable QString m_lastAppIdService; // 上次解析 appId 时的 service(用于缓存) + AppIdResolver *m_resolver = nullptr; // appId 解析器 + mutable QString m_appName; + mutable QString m_lastService; }; diff --git a/dconfig-center/dde-dconfig-daemon/dconfigserver.cpp b/dconfig-center/dde-dconfig-daemon/dconfigserver.cpp index ad9f68d..d9d859f 100644 --- a/dconfig-center/dde-dconfig-daemon/dconfigserver.cpp +++ b/dconfig-center/dde-dconfig-daemon/dconfigserver.cpp @@ -36,7 +36,8 @@ DSGConfigServer::DSGConfigServer(QObject *parent) :QObject (parent), m_watcher(nullptr), m_refManager(new RefManager(this)) - , m_syncRequestCache(new ConfigSyncRequestCache(this)) + , m_syncRequestCache(new ConfigSyncRequestCache(this)), + m_appIdResolver(new AppIdResolver(this)) { connect(this, &DSGConfigServer::releaseResource, this, &DSGConfigServer::onReleaseResource); connect(m_refManager, &RefManager::releaseResource, this, &DSGConfigServer::releaseResource); @@ -293,6 +294,11 @@ QDBusObjectPath DSGConfigServer::acquireManagerV2(const uint &uid, const QString qCInfo(cfLog, "Reuse connection:%s", qPrintable(conn->path())); } + // 设置配置文件的 appId(用于 private 权限校验) + QString configAppId = parseConfigAppId(name, subpath); + conn->setConfigAppId(configAppId); + conn->setAppIdResolver(m_appIdResolver); + if (resourceHolder) { m_resources.insert(genericResourceKey, resourceHolder.release()); QObject::connect(resource, &DSGConfigResource::releaseConn, this, &DSGConfigServer::onReleaseChanged); @@ -621,3 +627,37 @@ QVector DSGConfigServer::allConfigureFileSignatu return signatures; } + +AppIdResolver* DSGConfigServer::appIdResolver() const +{ + return m_appIdResolver; +} + +QString DSGConfigServer::parseConfigAppId(const QString &name, const QString &subpath) +{ + // 从配置文件路径解析 appId + // 配置文件路径格式: + // /usr/share/dsg/configs/{appId}/{name}/{subpath}.json + // /usr/share/dsg/configs/{name}/{subpath}.json (generic, 无 appId) + + // 尝试从各个 meta 目录查找配置文件 + const QStringList metaDirs = DConfigMeta::genericMetaDirs(m_localPrefix); + + for (const QString &dir : metaDirs) { + // 尝试带 appId 的路径 + QDirIterator it(dir, QStringList() << name + ".json", + QDir::Files | QDir::Readable, QDirIterator::Subdirectories); + while (it.hasNext()) { + QString path = it.next(); + ConfigureId configId = getMetaConfigureId(path); + if (configId.resource == name && configId.subpath == subpath) { + if (!configId.appid.isEmpty()) { + return configId.appid; + } + } + } + } + + // 未找到带 appId 的配置,返回空字符串表示 generic 配置 + return QString(); +} diff --git a/dconfig-center/dde-dconfig-daemon/dconfigserver.h b/dconfig-center/dde-dconfig-daemon/dconfigserver.h index f4127da..1688011 100644 --- a/dconfig-center/dde-dconfig-daemon/dconfigserver.h +++ b/dconfig-center/dde-dconfig-daemon/dconfigserver.h @@ -5,6 +5,7 @@ #pragma once #include "dconfig_global.h" +#include "appidresolver.h" #include #include #include @@ -41,6 +42,9 @@ class DSGConfigServer : public QObject, protected QDBusContext void setEnableExit(const bool enable); int resourceSize() const; + + // 获取 AppIdResolver(用于注入到 DSGConfigConn) + AppIdResolver* appIdResolver() const; Q_SIGNALS: void releaseResource(const ConnKey& resource); @@ -86,6 +90,9 @@ private Q_SLOTS: bool isConfigurePath(const QString &path, const QString& appId) const; std::optional updateInternal(const QString &path); + + // 从配置文件路径解析 appId + QString parseConfigAppId(const QString &name, const QString &subpath); // Reload interface related structures and methods struct FileSignature { @@ -108,6 +115,9 @@ private Q_SLOTS: bool m_enableExit = false; ConfigSyncRequestCache *m_syncRequestCache = nullptr; + // AppId 解析器 + AppIdResolver *m_appIdResolver = nullptr; + // Last time of the configuration file signature QVector m_fileSignatures; }; diff --git a/dconfig-center/dde-dconfig-daemon/src.cmake b/dconfig-center/dde-dconfig-daemon/src.cmake index 8b7701a..2bb7f63 100644 --- a/dconfig-center/dde-dconfig-daemon/src.cmake +++ b/dconfig-center/dde-dconfig-daemon/src.cmake @@ -26,12 +26,14 @@ include_directories(../common) set(HEADERS ${CMAKE_CURRENT_LIST_DIR}/dconfig_global.h + ${CMAKE_CURRENT_LIST_DIR}/appidresolver.h ${CMAKE_CURRENT_LIST_DIR}/dconfigserver.h ${CMAKE_CURRENT_LIST_DIR}/dconfigresource.h ${CMAKE_CURRENT_LIST_DIR}/dconfigconn.h ${CMAKE_CURRENT_LIST_DIR}/dconfigrefmanager.h ) set(SOURCES + ${CMAKE_CURRENT_LIST_DIR}/appidresolver.cpp ${CMAKE_CURRENT_LIST_DIR}/dconfigserver.cpp ${CMAKE_CURRENT_LIST_DIR}/dconfigresource.cpp ${CMAKE_CURRENT_LIST_DIR}/dconfigconn.cpp