Music Hub  ..
A session-wide music playback service
lomiri.cpp
Go to the documentation of this file.
1 /*
2  * Copyright © 2014 Canonical Ltd.
3  * Copyright © 2022 UBports Foundation.
4  *
5  * Contact: Alberto Mardegan <mardy@users.sourceforge.net>
6  *
7  * This program is free software: you can redistribute it and/or modify it
8  * under the terms of the GNU Lesser General Public License version 3,
9  * as published by the Free Software Foundation.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with this program. If not, see <http://www.gnu.org/licenses/>.
18  *
19  * Authored by: Thomas Voß <thomas.voss@canonical.com>
20  */
21 
22 #include "apparmor/lomiri.h"
23 
24 #include "logging.h"
25 
26 #include <QDBusMessage>
27 #include <QDBusPendingCall>
28 #include <QDBusPendingCallWatcher>
29 #include <QDBusReply>
30 #include <QDebug>
31 #include <QString>
32 #include <QUrl>
33 #include <QVariantList>
34 #include <QVariantMap>
35 
36 #include <glib.h>
37 #include <click.h>
38 
39 #include <sys/apparmor.h>
40 #include <unistd.h> // geteuid()
41 
43 namespace media = lomiri::MediaHubService;
44 namespace MediaHubService = apparmor::lomiri;
45 
46 namespace
47 {
48 
49 static constexpr std::size_t index_package{0};
50 static constexpr std::size_t index_app{1};
51 
52 // Returns true if the context name is a valid Lomiri app id.
53 // If it is, out is populated with the package and app name.
54 bool process_context_name(const QString &s, QStringList &out,
55  QString &pkg_name)
56 {
57  // See https://wiki.ubuntu.com/AppStore/Interfaces/ApplicationId.
58 
59  if (s == "messaging-app")
60  {
61  pkg_name = s;
62  return true;
63  }
64 
65 
66  out = s.split('_');
67  if (out.count() == 2 || out.count() == 3)
68  {
69  pkg_name = out[index_package];
70  return true;
71  }
72 
73  return false;
74 }
75 }
76 
77 apparmor::lomiri::Context::Context(const QString &name)
78  : apparmor::Context{name},
79  unconfined_{str() == lomiri::unconfined},
80  has_package_name_{process_context_name(str(), app_id_parts, pkg_name_)}
81 {
82  MH_DEBUG("apparmor profile name: %s", qUtf8Printable(name));
83  MH_DEBUG("is_unconfined(): %s", (is_unconfined() ? "true" : "false"));
84  MH_DEBUG("has_package_name(): %s", (has_package_name() ? "true" : "false"));
85  if (not is_unconfined() and not has_package_name()) {
86  MH_FATAL("apparmor::lomiri::Context: Invalid profile name %s", qUtf8Printable(str()));
87  }
88 }
89 
91 {
92  return unconfined_;
93 }
94 
96 {
97  return has_package_name_;
98 }
99 
101 {
102  return pkg_name_;
103 }
104 
106 {
107  switch (app_id_parts.count()) {
108  case 3:
109  return app_id_parts[2];
110  case 2:
111  return app_id_parts[1];
112  default:
113  return str();
114  }
115 }
116 
118 {
119  return app_id_parts.isEmpty() ?
120  str() : (app_id_parts[index_package] + "-" + app_id_parts[index_app]);
121 }
122 
124  m_connection(QDBusConnection::sessionBus())
125 {
126 }
127 
129  const QString &name,
131 {
132  const QString dbusServiceName =
133  qEnvironmentVariable("MEDIA_HUB_MOCKED_DBUS", "org.freedesktop.DBus");
134  QDBusMessage msg =
135  QDBusMessage::createMethodCall(dbusServiceName,
136  "/org/freedesktop/DBus",
137  "org.freedesktop.DBus",
138  "GetConnectionCredentials");
139  msg.setArguments({ name });
140  QDBusPendingCall call = QDBusConnection::sessionBus().asyncCall(msg);
141  QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(call);
142  QObject::connect(callWatcher, &QDBusPendingCallWatcher::finished,
143  [cb](QDBusPendingCallWatcher *callWatcher) {
144  QDBusReply<QVariantMap> reply(*callWatcher);
145  QString appId;
146  if (reply.isValid()) {
147  QVariantMap map = reply.value();
148  QByteArray context = map.value("LinuxSecurityLabel").toByteArray();
149  if (!context.isEmpty()) {
150  aa_splitcon(context.data(), NULL);
151  appId = QString::fromUtf8(context);
152  }
153  } else {
154  QDBusError error = reply.error();
155  qWarning() << "Error getting app ID:" << error.name() <<
156  error.message();
157  }
158  cb(apparmor::lomiri::Context(appId));
159  callWatcher->deleteLater();
160  });
161 }
162 
163 bool apparmor::lomiri::ExistingAuthenticator::is_click_package_path(const apparmor::lomiri::Context& context, const QString &path)
164 {
165  g_autoptr(ClickDB) db = 0;
166  g_autoptr(GError) error = 0;
167  g_autofree gchar *click_path = 0;
168  QString package = context.package_name();
169  QString version = context.package_version();
170 
171  db = click_db_new();
172  if (!db) {
173  qWarning() << "Failed to create ClickDB";
174  return false;
175  }
176  click_db_read(db, 0, &error);
177  if (error) {
178  qWarning() << "Error reading click DB:" << error->message;
179  return false;
180  }
181  click_path = click_db_get_path(db, package.toUtf8(), version.toUtf8(), &error);
182  if (error) {
183  MH_DEBUG("click package path could not be determined for %s: %s", qUtf8Printable(context.package_name()), error->message);
184  return false;
185  }
186 
187  return path.startsWith(QString(click_path));
188 }
189 
191 {
192  if (context.is_unconfined())
193  return Result{true, "Client allowed access since it's unconfined"};
194 
195  QString path = uri.path();
196  MH_DEBUG("context.profile_name(): %s", qUtf8Printable(context.profile_name()));
197  MH_DEBUG("parsed_uri.path: %s", qUtf8Printable(path));
198 
199  // All confined apps can access their own files
200  if (path.contains(".local/share/" + context.package_name() + "/") ||
201  path.contains(".cache/" + context.package_name() + "/") ||
202  path.contains("/run/user/" + QString::number(geteuid()) + "/confined/" + context.package_name()))
203  {
204  return Result
205  {
206  true,
207  "Client can access content in ~/.local/share/" + context.package_name() + " or ~/.cache/" + context.package_name()
208  };
209  }
210  // Check for trust-store compatible path name using full messaging-app profile_name
211  else if (context.package_name() == "messaging-app" &&
212  /* Since the full APP_ID is not available yet (see aa_query_file_path()), add an exception: */
213  (path.contains(".local/share/" + context.profile_name() + ".ubports/") ||
214  path.contains(".cache/" + context.profile_name() + ".ubports/")))
215  {
216  return Result
217  {
218  true,
219  "Client can access content in ~/.local/share/" + context.profile_name() + " or ~/.cache/" + context.profile_name()
220  };
221  }
222  else if (is_click_package_path(context, path))
223  {
224  return Result{true, "Client can access content in own opt directory"};
225  }
226  else if ((path.contains("/system/media/audio/ui/") ||
227  path.contains("/android/system/media/audio/ui/")) &&
228  context.package_name() == "camera.ubports")
229  {
230  return Result{true, "Camera app can access ui sounds"};
231  }
232 
233  // TODO: Check if the trust store previously allowed direct access to uri
234 
235  // Check in ~/Music and ~/Videos
236  // TODO: when the trust store lands, check it to see if this app can access the dirs and
237  // then remove the explicit whitelist of the music-app, and gallery-app
238  else if ((context.package_name() == "music.ubports" || context.package_name() == "gallery.ubports") &&
239  (path.contains("Music/") ||
240  path.contains("Videos/") ||
241  path.contains("/media")))
242  {
243  return Result{true, "Client can access content in ~/Music or ~/Videos"};
244  }
245  else if (path.contains("/usr/share/sounds"))
246  {
247  return Result{true, "Client can access content in /usr/share/sounds"};
248  }
249  else if (uri.scheme() == "http" ||
250  uri.scheme() == "https" ||
251  uri.scheme() == "rtsp")
252  {
253  return Result{true, "Client can access streaming content"};
254  }
255 
256  return Result{false, "Client is not allowed to access: " + uri.toString()};
257 }
lomiri::MediaHubService::apparmor::lomiri::Context::is_unconfined
bool is_unconfined() const
Definition: lomiri.cpp:90
lomiri::MediaHubService::apparmor::lomiri::DBusDaemonRequestContextResolver::DBusDaemonRequestContextResolver
DBusDaemonRequestContextResolver()
Definition: lomiri.cpp:123
lomiri::MediaHubService::apparmor::lomiri::Context::has_package_name
bool has_package_name() const
Definition: lomiri.cpp:95
lomiri::MediaHubService::apparmor::lomiri::Context::package_name
QString package_name() const
Definition: lomiri.cpp:100
lomiri::MediaHubService::apparmor::lomiri::RequestAuthenticator::Result
std::tuple< bool, QString > Result
Definition: lomiri.h:129
lomiri::MediaHubService::apparmor::lomiri::Context::package_version
QString package_version() const
Definition: lomiri.cpp:105
lomiri::MediaHubService::apparmor::lomiri::ExistingAuthenticator::authenticate_open_uri_request
Result authenticate_open_uri_request(const Context &, const QUrl &uri) override
Definition: lomiri.cpp:190
lomiri::MediaHubService
Definition: context.h:28
lomiri::MediaHubService::apparmor::lomiri::Context
Definition: lomiri.h:52
lomiri.h
lomiri::MediaHubService::apparmor
Definition: context.h:30
MH_DEBUG
#define MH_DEBUG(...)
Definition: logging.h:38
lomiri::MediaHubService::apparmor::lomiri::DBusDaemonRequestContextResolver::resolve_context_for_dbus_name_async
void resolve_context_for_dbus_name_async(const QString &name, ResolveCallback) override
Definition: lomiri.cpp:128
lomiri::MediaHubService::apparmor::lomiri::Context::profile_name
QString profile_name() const
Definition: lomiri.cpp:117
MH_FATAL
#define MH_FATAL(...)
Definition: logging.h:42
lomiri::MediaHubService::apparmor::lomiri::RequestContextResolver::ResolveCallback
std::function< void(const Context &)> ResolveCallback
Definition: lomiri.h:88