summaryrefslogtreecommitdiffstats
path: root/nixos/tests/userborn-migration.nix
blob: af41e561f33abd491306a35df913c302e6e81938 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
{ lib, ... }:

# Verifies that switching from update-users-groups.pl to userborn honours
# /var/lib/nixos/{uid,gid}-map, auto-subuid-map and declarative-{users,groups},
# so removed users keep their ids reserved and are not reassigned, and
# subordinate id allocations survive the migration.

{
  name = "userborn-migration";

  meta.maintainers = with lib.maintainers; [ rvdp ];

  nodes.machine = {
    services.userborn.enable = false;
    systemd.sysusers.enable = false;
    users.mutableUsers = true;

    users.users = {
      survivor = {
        isNormalUser = true;
        autoSubUidGidRange = true;
      };
      ghost = {
        isNormalUser = true;
      };
      intruder = {
        isNormalUser = true;
        # Only created under userborn. Must not get ghost's old uid.
        enable = lib.mkDefault false;
      };
    };

    specialisation = {
      # Still perl-managed. ghost is removed but kept in uid-map.
      base-ghost-removed.configuration = {
        users.users.ghost.enable = lib.mkForce false;
      };

      # Switch to userborn with ghost gone and a new unpinned user.
      userborn.configuration = {
        services.userborn.enable = lib.mkForce true;
        users.users.ghost.enable = lib.mkForce false;
        users.users.intruder.enable = true;
      };

      # ghost re-added under userborn. Must keep its original uid.
      userborn-revived.configuration = {
        services.userborn.enable = lib.mkForce true;
        users.users.intruder.enable = true;
      };

      # The migration service must be removable from the closure.
      userborn-without-import.configuration = {
        services.userborn.enable = lib.mkForce true;
        services.userborn.importLegacyState = false;
        users.users.intruder.enable = true;
      };
    };
  };

  testScript =
    # python
    ''
      import json

      machine.wait_for_unit("multi-user.target")

      def uid(name: str) -> int:
          return int(machine.succeed(f"id --user {name}").strip())

      def switch(specialisation: str) -> None:
          machine.succeed(
              f"/run/booted-system/specialisation/{specialisation}/bin/switch-to-configuration switch 2>&1 | tee /dev/stderr"
          )

      with subtest("perl: capture allocated state"):
          survivor_uid = uid("survivor")
          ghost_uid = uid("ghost")

          uid_map = json.loads(machine.succeed("cat /var/lib/nixos/uid-map"))
          t.assertEqual(uid_map["ghost"], ghost_uid)
          t.assertEqual(uid_map["survivor"], survivor_uid)

          survivor_subuid = machine.succeed("grep '^survivor:' /etc/subuid").strip()
          auto_subuid_map = json.loads(machine.succeed("cat /var/lib/nixos/auto-subuid-map"))
          t.assertEqual(auto_subuid_map["survivor"], int(survivor_subuid.split(":")[1]))

      with subtest("perl: remove ghost"):
          switch("base-ghost-removed")
          machine.fail("getent passwd ghost")

          uid_map = json.loads(machine.succeed("cat /var/lib/nixos/uid-map"))
          t.assertEqual(
              uid_map["ghost"], ghost_uid, "perl script must retain removed users in uid-map"
          )

      with subtest("userborn: legacy state is imported"):
          # The import only runs while userborn's state directory is absent.
          machine.fail("test -e /var/lib/userborn")
          switch("userborn")
          machine.succeed("test -d /var/lib/userborn")

          # ghost must be a locked stub with its original uid.
          ghost_passwd = machine.succeed("getent passwd ghost").strip()
          t.assertEqual(
              int(ghost_passwd.split(":")[2]),
              ghost_uid,
              f"ghost stub has wrong uid: {ghost_passwd}",
          )
          ghost_shadow = machine.succeed("getent shadow ghost").strip()
          t.assertTrue(
              ghost_shadow.split(":")[1].startswith("!"),
              f"ghost stub is not locked: {ghost_shadow}",
          )

          # previous-userborn.json is replaced by ExecStartPost after use,
          # so check the journal instead.
          machine.succeed(
              "journalctl -u userborn-import-legacy.service --grep 'synthesised.*previous-userborn.json'"
          )

      with subtest("userborn: subid allocations survive the migration"):
          t.assertEqual(
              machine.succeed("grep '^survivor:' /etc/subuid").strip(),
              survivor_subuid,
              "survivor's subuid range changed across migration",
          )
          t.assertEqual(
              machine.succeed("grep '^survivor:' /etc/subgid").strip(),
              survivor_subuid,
              "survivor's subgid range changed across migration",
          )

      with subtest("userborn: no uid collision for new user"):
          intruder_uid = uid("intruder")
          t.assertNotEqual(
              intruder_uid,
              ghost_uid,
              "intruder was allocated ghost's old uid; migration failed to reserve it",
          )
          t.assertEqual(uid("survivor"), survivor_uid, "survivor uid changed across migration")

      with subtest("userborn: revival keeps original uid"):
          switch("userborn-revived")
          t.assertEqual(
              uid("ghost"),
              ghost_uid,
              "ghost was not revived with its original uid",
          )

      with subtest("idempotency"):
          machine.succeed("systemctl restart userborn-import-legacy.service")
          result = machine.succeed(
              "systemctl show -p ConditionResult userborn-import-legacy.service"
          ).strip()
          t.assertEqual(result, "ConditionResult=no")

      with subtest("legacy import service can be excluded"):
          switch("userborn-without-import")
          machine.fail("systemctl cat userborn-import-legacy.service")
    '';
}