~click-hackers/click/trunk

« back to all changes in this revision

Viewing changes to lib/click/user.vala

  • Committer: Colin Watson
  • Date: 2014-03-03 17:01:37 UTC
  • mto: This revision was merged to the branch mainline in revision 354.
  • Revision ID: cjwatson@canonical.com-20140303170137-4gs1zjmqtgkvzphq
Move an initial core of functionality (database, hooks, osextras, query,
user) from Python into a new "libclick" library, allowing
performance-critical clients to avoid the cost of starting a new Python
interpreter (LP: #1282311).

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/* Copyright (C) 2013, 2014 Canonical Ltd.
 
2
 * Author: Colin Watson <cjwatson@ubuntu.com>
 
3
 *
 
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.
 
7
 *
 
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.
 
12
 *
 
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/>.
 
15
 */
 
16
 
 
17
/* Registry of user-installed Click packages.
 
18
 * 
 
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
 
25
 * installed.
 
26
 * 
 
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
 
33
 * symlinks per user.
 
34
 */
 
35
 
 
36
namespace Click {
 
37
 
 
38
/* Pseudo-usernames selected to be invalid as a real username, and alluding
 
39
 * to group syntaxes used in other systems.
 
40
 */
 
41
private const string ALL_USERS = "@all";
 
42
private const string GC_IN_USE_USER = "@gcinuse";
 
43
 
 
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.
 
47
 */
 
48
private const string HIDDEN_VERSION = "@hidden";
 
49
 
 
50
public errordomain UserError {
 
51
        /**
 
52
         * Failure to get password file entry.
 
53
         */
 
54
        GETPWNAM,
 
55
        /**
 
56
         * Failure to create database directory.
 
57
         */
 
58
        CREATE_DB,
 
59
        /**
 
60
         * Failure to set ownership of database directory.
 
61
         */
 
62
        CHOWN_DB,
 
63
        /**
 
64
         * Requested user does not exist.
 
65
         */
 
66
        NO_SUCH_USER,
 
67
        /**
 
68
         * Failure to drop privileges.
 
69
         */
 
70
        DROP_PRIVS,
 
71
        /**
 
72
         * Failure to regain privileges.
 
73
         */
 
74
        REGAIN_PRIVS,
 
75
        /**
 
76
         * Requested package is hidden.
 
77
         */
 
78
        HIDDEN_PACKAGE,
 
79
        /**
 
80
         * Requested package does not exist.
 
81
         */
 
82
        NO_SUCH_PACKAGE,
 
83
        /**
 
84
         * Failure to rename file.
 
85
         */
 
86
        RENAME
 
87
}
 
88
 
 
89
private string
 
90
db_top (string root)
 
91
{
 
92
        /* This is deliberately outside any user's home directory so that it
 
93
         * can safely be iterated etc. as root.
 
94
         */
 
95
        return Path.build_filename (root, ".click", "users");
 
96
}
 
97
 
 
98
private string
 
99
db_for_user (string root, string user)
 
100
{
 
101
        return Path.build_filename (db_top (root), user);
 
102
}
 
103
 
 
104
private void
 
105
try_create (string path) throws UserError
 
106
{
 
107
        if (DirUtils.create (path, 0777) < 0)
 
108
                throw new UserError.CREATE_DB
 
109
                        ("Cannot create database directory %s: %s",
 
110
                         path, strerror (errno));
 
111
}
 
112
 
 
113
private void
 
114
try_chown (string path, Posix.Passwd pw) throws UserError
 
115
{
 
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));
 
120
}
 
121
 
 
122
public class Users : Object {
 
123
        public DB db { private get; construct; }
 
124
        private unowned Posix.Passwd? click_pw;
 
125
 
 
126
        public Users (DB db)
 
127
        {
 
128
                Object (db: db);
 
129
                click_pw = null;
 
130
        }
 
131
 
 
132
        /**
 
133
         * get_click_pw:
 
134
         *
 
135
         * Returns: The password file entry for the `clickpkg` user.
 
136
         */
 
137
        private unowned Posix.Passwd
 
138
        get_click_pw () throws UserError
 
139
        {
 
140
                if (click_pw == null) {
 
141
                        errno = 0;
 
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));
 
147
                }
 
148
                return click_pw;
 
149
        }
 
150
 
 
151
        internal void
 
152
        ensure_db () throws UserError
 
153
        {
 
154
                var create = new List<string> ();
 
155
 
 
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);
 
161
                }
 
162
 
 
163
                foreach (var path in create) {
 
164
                        try_create (path);
 
165
                        if (Posix.geteuid () == 0)
 
166
                                try_chown (path, get_click_pw ());
 
167
                }
 
168
        }
 
169
 
 
170
        /**
 
171
         * get_user_names:
 
172
         *
 
173
         * Returns: A list of user names with registrations.
 
174
         */
 
175
        public List<string>
 
176
        get_user_names () throws Error
 
177
        {
 
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)) {
 
183
                                if (entry in seen)
 
184
                                        continue;
 
185
                                var path = Path.build_filename (users_db,
 
186
                                                                entry);
 
187
                                if (FileUtils.test (path, FileTest.IS_DIR)) {
 
188
                                        seen.add (entry.dup ());
 
189
                                        entries.prepend (entry.dup ());
 
190
                                }
 
191
                        }
 
192
                }
 
193
                entries.reverse ();
 
194
                return entries;
 
195
        }
 
196
 
 
197
        /**
 
198
         * get_user:
 
199
         * @user_name: A user name.
 
200
         *
 
201
         * Returns: (transfer full): A new #ClickUser instance for @user.
 
202
         */
 
203
        public User
 
204
        get_user (string user_name) throws Error
 
205
        {
 
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.
 
211
                                 */
 
212
                                return new User.for_user (db, user_name);
 
213
                }
 
214
                throw new UserError.NO_SUCH_USER(
 
215
                        "User %s does not exist in any database", user_name);
 
216
        }
 
217
}
 
218
 
 
219
public class User : Object {
 
220
        public DB db { private get; construct; }
 
221
        public string name { private get; construct; }
 
222
 
 
223
        private Users? users;
 
224
        private unowned Posix.Passwd? user_pw;
 
225
        private int dropped_privileges_count;
 
226
        private Posix.mode_t? old_umask;
 
227
 
 
228
        private User (DB db, string? name = null) {
 
229
                string real_name;
 
230
                if (name != null)
 
231
                        real_name = name;
 
232
                else
 
233
                        real_name = Environment.get_user_name ().dup ();
 
234
                Object (db: db, name: real_name);
 
235
                users = null;
 
236
                user_pw = null;
 
237
                dropped_privileges_count = 0;
 
238
                old_umask = null;
 
239
        }
 
240
 
 
241
        public User.for_user (DB db, string? name = null) {
 
242
                this (db, name);
 
243
        }
 
244
 
 
245
        public User.for_all_users (DB db) {
 
246
                this (db, ALL_USERS);
 
247
        }
 
248
 
 
249
        public User.for_gc_in_use (DB db) {
 
250
                this (db, GC_IN_USE_USER);
 
251
        }
 
252
 
 
253
        /**
 
254
         * True if and only if this user is a pseudo-user.
 
255
         */
 
256
        public bool is_pseudo_user { get { return name.has_prefix ("@"); } }
 
257
 
 
258
        /**
 
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.
 
261
         */
 
262
        public bool is_gc_in_use { get { return name == GC_IN_USE_USER; } }
 
263
 
 
264
        /**
 
265
         * get_user_pw:
 
266
         *
 
267
         * Returns: The password file entry for this user.
 
268
         */
 
269
        private unowned Posix.Passwd
 
270
        get_user_pw () throws UserError
 
271
        {
 
272
                assert (! is_pseudo_user);
 
273
 
 
274
                if (user_pw == null) {
 
275
                        errno = 0;
 
276
                        user_pw = Posix.getpwnam (name);
 
277
                        if (user_pw == null)
 
278
                                throw new UserError.GETPWNAM
 
279
                                     ("Cannot get password file entry for " +
 
280
                                      "%s: %s", name, strerror (errno));
 
281
                }
 
282
                return user_pw;
 
283
        }
 
284
 
 
285
        /**
 
286
         * get_overlay_db:
 
287
         *
 
288
         * Returns: The path to the overlay database for this user, i.e. the
 
289
         * path where new packages will be installed.
 
290
         */
 
291
        public string
 
292
        get_overlay_db ()
 
293
        {
 
294
                return db_for_user (db.overlay, name);
 
295
        }
 
296
 
 
297
        private void
 
298
        ensure_db () throws UserError
 
299
        {
 
300
                if (users == null)
 
301
                        users = new Users (db);
 
302
                users.ensure_db ();
 
303
                var path = get_overlay_db ();
 
304
                if (! FileUtils.test (path, FileTest.EXISTS)) {
 
305
                        try_create (path);
 
306
                        if (Posix.geteuid () == 0 && ! is_pseudo_user)
 
307
                                try_chown (path, get_user_pw ());
 
308
                }
 
309
        }
 
310
 
 
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
 
316
         * database.
 
317
         */
 
318
 
 
319
        private void
 
320
        priv_drop_failure (string name) throws UserError
 
321
        {
 
322
                throw new UserError.DROP_PRIVS
 
323
                        ("Cannot drop privileges (%s): %s",
 
324
                         name, strerror (errno));
 
325
        }
 
326
 
 
327
        internal void
 
328
        drop_privileges () throws UserError
 
329
        {
 
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
 
334
                         * correct.
 
335
                         */
 
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);
 
342
                }
 
343
 
 
344
                ++dropped_privileges_count;
 
345
        }
 
346
 
 
347
        private void
 
348
        priv_regain_failure (string name)
 
349
        {
 
350
                /* It is too dangerous to carry on from this point, even if
 
351
                 * the caller has an exception handler.
 
352
                 */
 
353
                error ("Cannot regain privileges (%s): %s",
 
354
                       name, strerror (errno));
 
355
        }
 
356
 
 
357
        internal void
 
358
        regain_privileges ()
 
359
        {
 
360
                --dropped_privileges_count;
 
361
 
 
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");
 
370
                }
 
371
        }
 
372
 
 
373
        private bool
 
374
        is_valid_link (string path)
 
375
        {
 
376
                if (! is_symlink (path))
 
377
                        return false;
 
378
 
 
379
                try {
 
380
                        var target = FileUtils.read_link (path);
 
381
                        return ! target.has_prefix ("@");
 
382
                } catch (FileError e) {
 
383
                        return false;
 
384
                }
 
385
        }
 
386
 
 
387
        private List<string>
 
388
        get_package_names_dropped () throws Error
 
389
        {
 
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)
 
396
                                        != null ||
 
397
                                    entry in hidden)
 
398
                                        continue;
 
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 ());
 
404
                        }
 
405
 
 
406
                        if (name != ALL_USERS) {
 
407
                                var all_users_db = db_for_user (db[i].root,
 
408
                                                                ALL_USERS);
 
409
                                foreach (var entry in Click.Dir.open
 
410
                                                (all_users_db)) {
 
411
                                        if (entries.find_custom (entry, strcmp)
 
412
                                                != null ||
 
413
                                            entry in hidden)
 
414
                                                continue;
 
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 ());
 
421
                                }
 
422
                        }
 
423
                }
 
424
                entries.reverse ();
 
425
                return entries;
 
426
        }
 
427
 
 
428
        /**
 
429
         * get_package_names:
 
430
         *
 
431
         * Returns: (transfer full): A list of package names installed for
 
432
         * this user.
 
433
         */
 
434
        public List<string>
 
435
        get_package_names () throws Error
 
436
        {
 
437
                drop_privileges ();
 
438
                try {
 
439
                        return get_package_names_dropped ();
 
440
                } finally {
 
441
                        regain_privileges ();
 
442
                }
 
443
        }
 
444
 
 
445
        /**
 
446
         * has_package_name:
 
447
         * @package: A package name.
 
448
         *
 
449
         * Returns: True if this user has a version of @package registered,
 
450
         * otherwise false.
 
451
         */
 
452
        public bool
 
453
        has_package_name (string package)
 
454
        {
 
455
                try {
 
456
                        get_version (package);
 
457
                        return true;
 
458
                } catch (UserError e) {
 
459
                        return false;
 
460
                }
 
461
        }
 
462
 
 
463
        /**
 
464
         * get_version:
 
465
         * @package: A package name.
 
466
         *
 
467
         * Returns: The version of @package registered for this user.
 
468
         */
 
469
        public string
 
470
        get_version (string package) throws UserError
 
471
        {
 
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);
 
475
                        drop_privileges ();
 
476
                        try {
 
477
                                if (is_valid_link (path)) {
 
478
                                        try {
 
479
                                                var target =
 
480
                                                        FileUtils.read_link
 
481
                                                        (path);
 
482
                                                return Path.get_basename
 
483
                                                        (target);
 
484
                                        } catch (FileError e) {
 
485
                                        }
 
486
                                } else if (is_symlink (path))
 
487
                                        throw new UserError.HIDDEN_PACKAGE
 
488
                                                ("%s is hidden for user %s",
 
489
                                                 package, name);
 
490
                        } finally {
 
491
                                regain_privileges ();
 
492
                        }
 
493
 
 
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)) {
 
497
                                try {
 
498
                                        var target = FileUtils.read_link
 
499
                                                (path);
 
500
                                        return Path.get_basename (target);
 
501
                                } catch (FileError e) {
 
502
                                }
 
503
                        } else if (is_symlink (path))
 
504
                                throw new UserError.HIDDEN_PACKAGE
 
505
                                        ("%s is hidden for all users",
 
506
                                         package);
 
507
                }
 
508
 
 
509
                throw new UserError.NO_SUCH_PACKAGE
 
510
                        ("%s does not exist in any database for user %s",
 
511
                         package, name);
 
512
        }
 
513
 
 
514
        /**
 
515
         * set_version:
 
516
         * @package: A package name.
 
517
         * @version: A version string.
 
518
         *
 
519
         * Register version @version of @package for this user.
 
520
         */
 
521
        public void
 
522
        set_version (string package, string version) throws Error
 
523
        {
 
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");
 
528
                ensure_db ();
 
529
                string? old_version = null;
 
530
                try {
 
531
                        old_version = get_version (package);
 
532
                } catch (UserError e) {
 
533
                }
 
534
                drop_privileges ();
 
535
                try {
 
536
                        var target = db.get_path (package, version);
 
537
                        bool done = false;
 
538
                        if (is_valid_link (path)) {
 
539
                                unlink_force (path);
 
540
                                try {
 
541
                                        if (get_version (package) == version)
 
542
                                                done = true;
 
543
                                } catch (UserError e) {
 
544
                                }
 
545
                        }
 
546
                        if (! done) {
 
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",
 
551
                                                 new_path, path,
 
552
                                                 strerror (errno));
 
553
                        }
 
554
                } finally {
 
555
                        regain_privileges ();
 
556
                }
 
557
                if (! is_pseudo_user)
 
558
                        package_install_hooks (db, package,
 
559
                                               old_version, version, name);
 
560
        }
 
561
 
 
562
        /**
 
563
         * remove:
 
564
         * @package: A package name.
 
565
         *
 
566
         * Remove this user's registration of @package.
 
567
         */
 
568
        public void
 
569
        remove (string package) throws Error
 
570
        {
 
571
                /* Only modify the last database. */
 
572
                var user_db = get_overlay_db ();
 
573
                var path = Path.build_filename (user_db, package);
 
574
                string old_version;
 
575
                if (is_valid_link (path)) {
 
576
                        var target = FileUtils.read_link (path);
 
577
                        old_version = Path.get_basename (target);
 
578
                        drop_privileges ();
 
579
                        try {
 
580
                                unlink_force (path);
 
581
                        } finally {
 
582
                                regain_privileges ();
 
583
                        }
 
584
                } else {
 
585
                        try {
 
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);
 
591
                        }
 
592
                        ensure_db ();
 
593
                        drop_privileges ();
 
594
                        try {
 
595
                                symlink_force (HIDDEN_VERSION, path);
 
596
                        } finally {
 
597
                                regain_privileges ();
 
598
                        }
 
599
                }
 
600
                if (! is_pseudo_user)
 
601
                        package_remove_hooks (db, package, old_version, name);
 
602
        }
 
603
 
 
604
        /**
 
605
         * get_path:
 
606
         * @package: A package name.
 
607
         *
 
608
         * Returns: The path at which @package is registered for this user.
 
609
         */
 
610
        public string
 
611
        get_path (string package) throws UserError
 
612
        {
 
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))
 
617
                                return path;
 
618
                        else if (is_symlink (path))
 
619
                                throw new UserError.HIDDEN_PACKAGE
 
620
                                        ("%s is hidden for user %s",
 
621
                                         package, name);
 
622
 
 
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))
 
626
                                return path;
 
627
                        else if (is_symlink (path))
 
628
                                throw new UserError.HIDDEN_PACKAGE
 
629
                                        ("%s is hidden for all users",
 
630
                                         package);
 
631
                }
 
632
 
 
633
                throw new UserError.NO_SUCH_PACKAGE
 
634
                        ("%s does not exist in any database for user %s",
 
635
                         package, name);
 
636
        }
 
637
 
 
638
        /**
 
639
         * is_removable:
 
640
         * @package: A package name.
 
641
         *
 
642
         * Returns: True if @package is removable for this user, otherwise
 
643
         * False.
 
644
         */
 
645
        public bool
 
646
        is_removable (string package)
 
647
        {
 
648
                var user_db = get_overlay_db ();
 
649
                var path = Path.build_filename (user_db, package);
 
650
                if (FileUtils.test (path, FileTest.EXISTS))
 
651
                        return true;
 
652
                else if (is_symlink (path))
 
653
                        /* Already hidden. */
 
654
                        return false;
 
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))
 
658
                        return true;
 
659
                else if (is_symlink (path))
 
660
                        /* Already hidden. */
 
661
                        return false;
 
662
                string? version = null;
 
663
                try {
 
664
                        version = get_version (package);
 
665
                } catch (UserError e) {
 
666
                }
 
667
                if (version != null)
 
668
                        /* Not in overlay database, but can be hidden. */
 
669
                        return true;
 
670
                else
 
671
                        return false;
 
672
        }
 
673
}
 
674
 
 
675
}