1
/* Copyright (C) 2013, 2014 Canonical Ltd.
2
* Author: Colin Watson <cjwatson@ubuntu.com>
4
* This program is free software: you can redistribute it and/or modify
5
* it under the terms of the GNU General Public License as published by
6
* the Free Software Foundation; version 3 of the License.
8
* This program is distributed in the hope that it will be useful,
9
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
* GNU General Public License for more details.
13
* You should have received a copy of the GNU General Public License
14
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17
/* Registry of user-installed Click packages.
19
* Click packages are installed into per-package/version directories, so it
20
* is quite feasible for more than one version of a given package to be
21
* installed at once, allowing per-user installations; for instance, one
22
* user of a tablet may be uncomfortable with granting some new permission
23
* to an app, but another may be just fine with it. To make this useful, we
24
* also need a registry of which users have which versions of each package
27
* We might have chosen to use a proper database. However, a major goal of
28
* Click packages is extreme resilience; we must never get into a situation
29
* where some previous error in package installation or removal makes it
30
* hard for the user to install or remove other packages. Furthermore, the
31
* simpler application execution can be the better. So, instead, we use
32
* just about the simplest "database" format imaginable: a directory of
38
/* Pseudo-usernames selected to be invalid as a real username, and alluding
39
* to group syntaxes used in other systems.
41
private const string ALL_USERS = "@all";
42
private const string GC_IN_USE_USER = "@gcinuse";
44
/* Pseudo-versions. In this case the @ doesn't allude to group syntaxes,
45
* but since @ is conveniently invalid in version numbers we stick to the
46
* same prefix used for pseudo-usernames.
48
private const string HIDDEN_VERSION = "@hidden";
50
public errordomain UserError {
52
* Failure to get password file entry.
56
* Failure to create database directory.
60
* Failure to set ownership of database directory.
64
* Requested user does not exist.
68
* Failure to drop privileges.
72
* Failure to regain privileges.
76
* Requested package is hidden.
80
* Requested package does not exist.
84
* Failure to rename file.
92
/* This is deliberately outside any user's home directory so that it
93
* can safely be iterated etc. as root.
95
return Path.build_filename (root, ".click", "users");
99
db_for_user (string root, string user)
101
return Path.build_filename (db_top (root), user);
105
try_create (string path) throws UserError
107
if (DirUtils.create (path, 0777) < 0)
108
throw new UserError.CREATE_DB
109
("Cannot create database directory %s: %s",
110
path, strerror (errno));
114
try_chown (string path, Posix.Passwd pw) throws UserError
116
if (Posix.chown (path, pw.pw_uid, pw.pw_gid) < 0)
117
throw new UserError.CHOWN_DB
118
("Cannot set ownership of database directory %s: %s",
119
path, strerror (errno));
122
public class Users : Object {
123
public DB db { private get; construct; }
124
private unowned Posix.Passwd? click_pw;
135
* Returns: The password file entry for the `clickpkg` user.
137
private unowned Posix.Passwd
138
get_click_pw () throws UserError
140
if (click_pw == null) {
142
click_pw = Posix.getpwnam ("clickpkg");
143
if (click_pw == null)
144
throw new UserError.GETPWNAM
145
("Cannot get password file entry " +
146
"for clickpkg: %s", strerror (errno));
152
ensure_db () throws UserError
154
var create = new List<string> ();
156
/* Only modify the last database. */
157
var try_path = db_top (db.overlay);
158
while (! FileUtils.test (try_path, FileTest.EXISTS)) {
159
create.prepend (try_path);
160
try_path = Path.get_dirname (try_path);
163
foreach (var path in create) {
165
if (Posix.geteuid () == 0)
166
try_chown (path, get_click_pw ());
173
* Returns: A list of user names with registrations.
176
get_user_names () throws Error
178
var entries = new List<string> ();
179
var seen = new Gee.HashSet<string> ();
180
foreach (var single_db in db) {
181
var users_db = db_top (single_db.root);
182
foreach (var entry in Click.Dir.open (users_db)) {
185
var path = Path.build_filename (users_db,
187
if (FileUtils.test (path, FileTest.IS_DIR)) {
188
seen.add (entry.dup ());
189
entries.prepend (entry.dup ());
199
* @user_name: A user name.
201
* Returns: (transfer full): A new #ClickUser instance for @user.
204
get_user (string user_name) throws Error
206
foreach (var single_db in db) {
207
var path = db_for_user (single_db.root, user_name);
208
if (FileUtils.test (path, FileTest.IS_DIR))
209
/* We only require the user path to exist in
210
* any database; it doesn't matter which.
212
return new User.for_user (db, user_name);
214
throw new UserError.NO_SUCH_USER(
215
"User %s does not exist in any database", user_name);
219
public class User : Object {
220
public DB db { private get; construct; }
221
public string name { private get; construct; }
223
private Users? users;
224
private unowned Posix.Passwd? user_pw;
225
private int dropped_privileges_count;
226
private Posix.mode_t? old_umask;
228
private User (DB db, string? name = null) {
233
real_name = Environment.get_user_name ().dup ();
234
Object (db: db, name: real_name);
237
dropped_privileges_count = 0;
241
public User.for_user (DB db, string? name = null) {
245
public User.for_all_users (DB db) {
246
this (db, ALL_USERS);
249
public User.for_gc_in_use (DB db) {
250
this (db, GC_IN_USE_USER);
254
* True if and only if this user is a pseudo-user.
256
public bool is_pseudo_user { get { return name.has_prefix ("@"); } }
259
* True if and only if this user is the pseudo-user indicating that
260
* a registration was in use at the time of package removal.
262
public bool is_gc_in_use { get { return name == GC_IN_USE_USER; } }
267
* Returns: The password file entry for this user.
269
private unowned Posix.Passwd
270
get_user_pw () throws UserError
272
assert (! is_pseudo_user);
274
if (user_pw == null) {
276
user_pw = Posix.getpwnam (name);
278
throw new UserError.GETPWNAM
279
("Cannot get password file entry for " +
280
"%s: %s", name, strerror (errno));
288
* Returns: The path to the overlay database for this user, i.e. the
289
* path where new packages will be installed.
294
return db_for_user (db.overlay, name);
298
ensure_db () throws UserError
301
users = new Users (db);
303
var path = get_overlay_db ();
304
if (! FileUtils.test (path, FileTest.EXISTS)) {
306
if (Posix.geteuid () == 0 && ! is_pseudo_user)
307
try_chown (path, get_user_pw ());
311
/* Note on privilege handling:
312
* We can normally get away without dropping privilege when reading,
313
* but some filesystems are strict about how much they let root work
314
* with user files (e.g. NFS root_squash). It is better to play it
315
* safe and drop privileges for any operations on the user's
320
priv_drop_failure (string name) throws UserError
322
throw new UserError.DROP_PRIVS
323
("Cannot drop privileges (%s): %s",
324
name, strerror (errno));
328
drop_privileges () throws UserError
330
if (dropped_privileges_count == 0 &&
331
Posix.getuid () == 0 && ! is_pseudo_user) {
332
/* We don't bother with setgroups here; we only need
333
* the user/group of created filesystem nodes to be
336
unowned Posix.Passwd? pw = get_user_pw ();
337
if (PosixExtra.setegid (pw.pw_gid) < 0)
338
priv_drop_failure ("setegid");
339
if (PosixExtra.seteuid (pw.pw_uid) < 0)
340
priv_drop_failure ("seteuid");
341
old_umask = Posix.umask (get_umask () | Posix.S_IWOTH);
344
++dropped_privileges_count;
348
priv_regain_failure (string name)
350
/* It is too dangerous to carry on from this point, even if
351
* the caller has an exception handler.
353
error ("Cannot regain privileges (%s): %s",
354
name, strerror (errno));
360
--dropped_privileges_count;
362
if (dropped_privileges_count == 0 &&
363
Posix.getuid () == 0 && ! is_pseudo_user) {
364
if (old_umask != null)
365
Posix.umask (old_umask);
366
if (PosixExtra.seteuid (0) < 0)
367
priv_regain_failure ("seteuid");
368
if (PosixExtra.setegid (0) < 0)
369
priv_regain_failure ("setegid");
374
is_valid_link (string path)
376
if (! is_symlink (path))
380
var target = FileUtils.read_link (path);
381
return ! target.has_prefix ("@");
382
} catch (FileError e) {
388
get_package_names_dropped () throws Error
390
var entries = new List<string> ();
391
var hidden = new Gee.HashSet<string> ();
392
for (int i = db.size - 1; i >= 0; --i) {
393
var user_db = db_for_user (db[i].root, name);
394
foreach (var entry in Click.Dir.open (user_db)) {
395
if (entries.find_custom (entry, strcmp)
399
var path = Path.build_filename (user_db, entry);
400
if (is_valid_link (path))
401
entries.prepend (entry.dup ());
402
else if (is_symlink (path))
403
hidden.add (entry.dup ());
406
if (name != ALL_USERS) {
407
var all_users_db = db_for_user (db[i].root,
409
foreach (var entry in Click.Dir.open
411
if (entries.find_custom (entry, strcmp)
415
var path = Path.build_filename
416
(all_users_db, entry);
417
if (is_valid_link (path))
418
entries.prepend (entry.dup ());
419
else if (is_symlink (path))
420
hidden.add (entry.dup ());
431
* Returns: (transfer full): A list of package names installed for
435
get_package_names () throws Error
439
return get_package_names_dropped ();
441
regain_privileges ();
447
* @package: A package name.
449
* Returns: True if this user has a version of @package registered,
453
has_package_name (string package)
456
get_version (package);
458
} catch (UserError e) {
465
* @package: A package name.
467
* Returns: The version of @package registered for this user.
470
get_version (string package) throws UserError
472
for (int i = db.size - 1; i >= 0; --i) {
473
var user_db = db_for_user (db[i].root, name);
474
var path = Path.build_filename (user_db, package);
477
if (is_valid_link (path)) {
482
return Path.get_basename
484
} catch (FileError e) {
486
} else if (is_symlink (path))
487
throw new UserError.HIDDEN_PACKAGE
488
("%s is hidden for user %s",
491
regain_privileges ();
494
var all_users_db = db_for_user (db[i].root, ALL_USERS);
495
path = Path.build_filename (all_users_db, package);
496
if (is_valid_link (path)) {
498
var target = FileUtils.read_link
500
return Path.get_basename (target);
501
} catch (FileError e) {
503
} else if (is_symlink (path))
504
throw new UserError.HIDDEN_PACKAGE
505
("%s is hidden for all users",
509
throw new UserError.NO_SUCH_PACKAGE
510
("%s does not exist in any database for user %s",
516
* @package: A package name.
517
* @version: A version string.
519
* Register version @version of @package for this user.
522
set_version (string package, string version) throws Error
524
/* Only modify the last database. */
525
var user_db = get_overlay_db ();
526
var path = Path.build_filename (user_db, package);
527
var new_path = Path.build_filename (user_db, @".$package.new");
529
string? old_version = null;
531
old_version = get_version (package);
532
} catch (UserError e) {
536
var target = db.get_path (package, version);
538
if (is_valid_link (path)) {
541
if (get_version (package) == version)
543
} catch (UserError e) {
547
symlink_force (target, new_path);
548
if (FileUtils.rename (new_path, path) < 0)
549
throw new UserError.RENAME
550
("rename %s -> %s failed: %s",
555
regain_privileges ();
557
if (! is_pseudo_user)
558
package_install_hooks (db, package,
559
old_version, version, name);
564
* @package: A package name.
566
* Remove this user's registration of @package.
569
remove (string package) throws Error
571
/* Only modify the last database. */
572
var user_db = get_overlay_db ();
573
var path = Path.build_filename (user_db, package);
575
if (is_valid_link (path)) {
576
var target = FileUtils.read_link (path);
577
old_version = Path.get_basename (target);
582
regain_privileges ();
586
old_version = get_version (package);
587
} catch (UserError e) {
588
throw new UserError.NO_SUCH_PACKAGE
589
("%s does not exist in any database " +
590
"for user %s", package, name);
595
symlink_force (HIDDEN_VERSION, path);
597
regain_privileges ();
600
if (! is_pseudo_user)
601
package_remove_hooks (db, package, old_version, name);
606
* @package: A package name.
608
* Returns: The path at which @package is registered for this user.
611
get_path (string package) throws UserError
613
for (int i = db.size - 1; i >= 0; --i) {
614
var user_db = db_for_user (db[i].root, name);
615
var path = Path.build_filename (user_db, package);
616
if (is_valid_link (path))
618
else if (is_symlink (path))
619
throw new UserError.HIDDEN_PACKAGE
620
("%s is hidden for user %s",
623
var all_users_db = db_for_user (db[i].root, ALL_USERS);
624
path = Path.build_filename (all_users_db, package);
625
if (is_valid_link (path))
627
else if (is_symlink (path))
628
throw new UserError.HIDDEN_PACKAGE
629
("%s is hidden for all users",
633
throw new UserError.NO_SUCH_PACKAGE
634
("%s does not exist in any database for user %s",
640
* @package: A package name.
642
* Returns: True if @package is removable for this user, otherwise
646
is_removable (string package)
648
var user_db = get_overlay_db ();
649
var path = Path.build_filename (user_db, package);
650
if (FileUtils.test (path, FileTest.EXISTS))
652
else if (is_symlink (path))
653
/* Already hidden. */
655
var all_users_db = db_for_user (db.overlay, ALL_USERS);
656
path = Path.build_filename (all_users_db, package);
657
if (is_valid_link (path))
659
else if (is_symlink (path))
660
/* Already hidden. */
662
string? version = null;
664
version = get_version (package);
665
} catch (UserError e) {
668
/* Not in overlay database, but can be hidden. */