summary refs log tree commit diff
path: root/nixos/modules/config/users-groups.nix
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/config/users-groups.nix')
-rw-r--r--nixos/modules/config/users-groups.nix325
1 files changed, 325 insertions, 0 deletions
diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix
new file mode 100644
index 000000000000..fb8b0229c1da
--- /dev/null
+++ b/nixos/modules/config/users-groups.nix
@@ -0,0 +1,325 @@
+{pkgs, config, ...}:
+
+with pkgs.lib;
+
+let
+
+  ids = config.ids;
+  users = config.users;
+
+  userOpts = { name, config, ... }: {
+
+    options = {
+
+      name = mkOption {
+        type = types.str;
+        description = "The name of the user account. If undefined, the name of the attribute set will be used.";
+      };
+
+      description = mkOption {
+        type = types.str;
+        default = "";
+        example = "Alice Q. User";
+        description = ''
+          A short description of the user account, typically the
+          user's full name.  This is actually the “GECOS” or “comment”
+          field in <filename>/etc/passwd</filename>.
+        '';
+      };
+
+      uid = mkOption {
+        type = with types; uniq (nullOr int);
+        default = null;
+        description = "The account UID. If undefined, NixOS will select a free UID.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "nogroup";
+        description = "The user's primary group.";
+      };
+
+      extraGroups = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "The user's auxiliary groups.";
+      };
+
+      home = mkOption {
+        type = types.str;
+        default = "/var/empty";
+        description = "The user's home directory.";
+      };
+
+      shell = mkOption {
+        type = types.str;
+        default = "/run/current-system/sw/sbin/nologin";
+        description = "The path to the user's shell.";
+      };
+
+      createHome = mkOption {
+        type = types.bool;
+        default = false;
+        description = "If true, the home directory will be created automatically.";
+      };
+
+      useDefaultShell = mkOption {
+        type = types.bool;
+        default = false;
+        description = "If true, the user's shell will be set to <literal>users.defaultUserShell</literal>.";
+      };
+
+      password = mkOption {
+        type = with types; uniq (nullOr str);
+        default = null;
+        description = ''
+          The user's password. If undefined, no password is set for
+          the user.  Warning: do not set confidential information here
+          because it is world-readable in the Nix store.  This option
+          should only be used for public accounts such as
+          <literal>guest</literal>.
+        '';
+      };
+
+      isSystemUser = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Indicates if the user is a system user or not.";
+      };
+
+      createUser = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Indicates if the user should be created automatically as a local user.
+          Set this to false if the user for instance is an LDAP user. NixOS will
+          then not modify any of the basic properties for the user account.
+        '';
+      };
+
+      isAlias = mkOption {
+        type = types.bool;
+        default = false;
+        description = "If true, the UID of this user is not required to be unique and can thus alias another user.";
+      };
+
+    };
+
+    config = {
+      name = mkDefault name;
+      uid = mkDefault (attrByPath [name] null ids.uids);
+      shell = mkIf config.useDefaultShell (mkDefault users.defaultUserShell);
+    };
+
+  };
+
+  groupOpts = { name, config, ... }: {
+
+    options = {
+
+      name = mkOption {
+        type = types.str;
+        description = "The name of the group. If undefined, the name of the attribute set will be used.";
+      };
+
+      gid = mkOption {
+        type = with types; uniq (nullOr int);
+        default = null;
+        description = "The GID of the group. If undefined, NixOS will select a free GID.";
+      };
+
+    };
+
+    config = {
+      name = mkDefault name;
+      gid = mkDefault (attrByPath [name] null ids.gids);
+    };
+
+  };
+
+  # Note: the 'X' in front of the password is to distinguish between
+  # having an empty password, and not having a password.
+  serializedUser = u: "${u.name}\n${u.description}\n${if u.uid != null then toString u.uid else ""}\n${u.group}\n${toString (concatStringsSep "," u.extraGroups)}\n${u.home}\n${u.shell}\n${toString u.createHome}\n${if u.password != null then "X" + u.password else ""}\n${toString u.isSystemUser}\n${toString u.createUser}\n${toString u.isAlias}\n";
+
+  usersFile = pkgs.writeText "users" (
+    let
+      p = partition (u: u.isAlias) (attrValues config.users.extraUsers);
+    in concatStrings (map serializedUser p.wrong ++ map serializedUser p.right));
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    users.extraUsers = mkOption {
+      default = {};
+      type = types.loaOf types.optionSet;
+      example = {
+        alice = {
+          uid = 1234;
+          description = "Alice Q. User";
+          home = "/home/alice";
+          createHome = true;
+          group = "users";
+          extraGroups = ["wheel"];
+          shell = "/bin/sh";
+        };
+      };
+      description = ''
+        Additional user accounts to be created automatically by the system.
+        This can also be used to set options for root.
+      '';
+      options = [ userOpts ];
+    };
+
+    users.extraGroups = mkOption {
+      default = {};
+      example =
+        { students.gid = 1001;
+          hackers = { };
+        };
+      type = types.loaOf types.optionSet;
+      description = ''
+        Additional groups to be created automatically by the system.
+      '';
+      options = [ groupOpts ];
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = {
+
+    users.extraUsers = {
+      root = {
+        description = "System administrator";
+        home = "/root";
+        shell = config.users.defaultUserShell;
+        group = "root";
+      };
+      nobody = {
+        description = "Unprivileged account (don't use!)";
+      };
+    };
+
+    users.extraGroups = {
+      root = { };
+      wheel = { };
+      disk = { };
+      kmem = { };
+      tty = { };
+      floppy = { };
+      uucp = { };
+      lp = { };
+      cdrom = { };
+      tape = { };
+      audio = { };
+      video = { };
+      dialout = { };
+      nogroup = { };
+      users = { };
+      nixbld = { };
+      utmp = { };
+      adm = { }; # expected by journald
+    };
+
+    system.activationScripts.rootPasswd = stringAfter [ "etc" ]
+      ''
+        # If there is no password file yet, create a root account with an
+        # empty password.
+        if ! test -e /etc/passwd; then
+            rootHome=/root
+            touch /etc/passwd; chmod 0644 /etc/passwd
+            touch /etc/group; chmod 0644 /etc/group
+            touch /etc/shadow; chmod 0600 /etc/shadow
+            # Can't use useradd, since it complains that it doesn't know us
+            # (bootstrap problem!).
+            echo "root:x:0:0:System administrator:$rootHome:${config.users.defaultUserShell}" >> /etc/passwd
+            echo "root::::::::" >> /etc/shadow
+        fi
+      '';
+
+    system.activationScripts.users = stringAfter [ "groups" ]
+      ''
+        echo "updating users..."
+
+        cat ${usersFile} | while true; do
+            read name || break
+            read description
+            read uid
+            read group
+            read extraGroups
+            read home
+            read shell
+            read createHome
+            read password
+            read isSystemUser
+            read createUser
+            read isAlias
+
+            if [ -z "$createUser" ]; then
+                continue
+            fi
+
+            if ! curEnt=$(getent passwd "$name"); then
+                useradd ''${isSystemUser:+--system} \
+                    --comment "$description" \
+                    ''${uid:+--uid $uid} \
+                    --gid "$group" \
+                    --groups "$extraGroups" \
+                    --home "$home" \
+                    --shell "$shell" \
+                    ''${createHome:+--create-home} \
+                    ''${isAlias:+--non-unique} \
+                    "$name"
+                if test "''${password:0:1}" = 'X'; then
+                    (echo "''${password:1}"; echo "''${password:1}") | ${pkgs.shadow}/bin/passwd "$name"
+                fi
+            else
+                #echo "updating user $name..."
+                oldIFS="$IFS"; IFS=:; set -- $curEnt; IFS="$oldIFS"
+                prevUid=$3
+                prevHome=$6
+                # Don't change the home directory if it's the same to prevent
+                # unnecessary warnings about logged in users.
+                if test "$prevHome" = "$home"; then unset home; fi
+                usermod \
+                    --comment "$description" \
+                    --gid "$group" \
+                    --groups "$extraGroups" \
+                    ''${home:+--home "$home"} \
+                    --shell "$shell" \
+                    "$name"
+            fi
+
+        done
+      '';
+
+    system.activationScripts.groups = stringAfter [ "rootPasswd" "binsh" "etc" "var" ]
+      ''
+        echo "updating groups..."
+
+        createGroup() {
+            name="$1"
+            gid="$2"
+
+            if ! curEnt=$(getent group "$name"); then
+                groupadd --system \
+                    ''${gid:+--gid $gid} \
+                    "$name"
+            fi
+        }
+
+        ${flip concatMapStrings (attrValues config.users.extraGroups) (g: ''
+          createGroup '${g.name}' '${toString g.gid}'
+        '')}
+      '';
+
+  };
+
+}