system: Change the shell of 'nobody' to 'nologin'.
[jackhill/guix/guix.git] / gnu / system / shadow.scm
index 2a85a20..593117e 100644 (file)
@@ -1,5 +1,6 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2013 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2013, 2014, 2015, 2016 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2016 Alex Griffin <a@ajgrf.com>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
 ;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
 
 (define-module (gnu system shadow)
-  #:use-module (guix store)
   #:use-module (guix records)
-  #:use-module (guix packages)
-  #:use-module (guix monads)
+  #:use-module (guix gexp)
+  #:use-module (guix store)
+  #:use-module (guix sets)
+  #:use-module (guix ui)
+  #:use-module (gnu services)
+  #:use-module ((gnu system file-systems)
+                #:select (%tty-gid))
   #:use-module ((gnu packages admin)
                 #:select (shadow))
   #:use-module (gnu packages bash)
+  #:use-module (gnu packages guile-wm)
   #:use-module (srfi srfi-1)
-  #:use-module (ice-9 match)
+  #:use-module (srfi srfi-26)
+  #:use-module (srfi srfi-34)
+  #:use-module (srfi srfi-35)
   #:export (user-account
             user-account?
             user-account-name
-            user-account-pass
+            user-account-password
             user-account-uid
-            user-account-gid
+            user-account-group
+            user-account-supplementary-groups
             user-account-comment
             user-account-home-directory
             user-account-shell
-            user-account-inputs
+            user-account-system?
 
             user-group
             user-group?
             user-group-name
             user-group-password
             user-group-id
-            user-group-members
+            user-group-system?
+
+            default-skeletons
+            skeleton-directory
+            %base-groups
+            %base-user-accounts
 
-            passwd-file
-            group-file
-            guix-build-accounts))
+            account-service-type
+            account-service))
 
 ;;; Commentary:
 ;;;
   user-account make-user-account
   user-account?
   (name           user-account-name)
-  (password       user-account-pass (default ""))
-  (uid            user-account-uid)
-  (gid            user-account-gid)
+  (password       user-account-password (default #f))
+  (uid            user-account-uid (default #f))
+  (group          user-account-group)             ; number | string
+  (supplementary-groups user-account-supplementary-groups
+                        (default '()))            ; list of strings
   (comment        user-account-comment (default ""))
   (home-directory user-account-home-directory)
-  (shell          user-account-shell              ; monadic value
-                  (default (package-file bash "bin/bash")))
-  (inputs         user-account-inputs (default `(("bash" ,bash)))))
+  (shell          user-account-shell              ; gexp
+                  (default #~(string-append #$bash "/bin/bash")))
+  (system?        user-account-system?            ; Boolean
+                  (default #f)))
 
 (define-record-type* <user-group>
   user-group make-user-group
   user-group?
   (name           user-group-name)
   (password       user-group-password (default #f))
-  (id             user-group-id)
-  (members        user-group-members (default '())))
-
-(define (group-file groups)
-  "Return a /etc/group file for GROUPS, a list of <user-group> objects."
-  (define contents
-    (let loop ((groups groups)
-               (result '()))
-      (match groups
-        ((($ <user-group> name _ gid (users ...)) rest ...)
-         ;; XXX: Ignore the group password.
-         (loop rest
-               (cons (string-append name "::" (number->string gid)
-                                    ":" (string-join users ","))
-                     result)))
-        (()
-         (string-join (reverse result) "\n" 'suffix)))))
-
-  (text-file "group" contents))
-
-(define* (passwd-file accounts #:key shadow?)
-  "Return a password file for ACCOUNTS, a list of <user-account> objects.  If
-SHADOW? is true, then it is a /etc/shadow file, otherwise it is a /etc/passwd
-file."
-  ;; XXX: The resulting file is world-readable, so beware when SHADOW? is #t!
-  (define (contents)
-    (with-monad %store-monad
-      (let loop ((accounts accounts)
-                 (result   '()))
-        (match accounts
-          ((($ <user-account> name pass uid gid comment home-dir mshell)
-            rest ...)
-           (mlet %store-monad ((shell mshell))
-             (loop rest
-                   (cons (if shadow?
-                             (string-append name
-                                            ":"    ; XXX: use (crypt PASS …)?
-                                            ":::::::")
-                             (string-append name
-                                            ":" "x"
-                                            ":" (number->string uid)
-                                            ":" (number->string gid)
-                                            ":" comment ":" home-dir ":" shell))
-                         result))))
-          (()
-           (return (string-join (reverse result) "\n" 'suffix)))))))
-
-  (mlet %store-monad ((contents (contents)))
-    (text-file (if shadow? "shadow" "passwd") contents)))
+  (id             user-group-id (default #f))
+  (system?        user-group-system?              ; Boolean
+                  (default #f)))
+
+
+(define %base-groups
+  ;; Default set of groups.
+  (let-syntax ((system-group (syntax-rules ()
+                               ((_ args ...)
+                                (user-group (system? #t) args ...)))))
+    (list (system-group (name "root") (id 0))
+          (system-group (name "wheel"))           ; root-like users
+          (system-group (name "users"))           ; normal users
+          (system-group (name "nogroup"))         ; for daemons etc.
+
+          ;; The following groups are conventionally used by things like udev to
+          ;; control access to hardware devices.
+          (system-group (name "tty") (id %tty-gid))
+          (system-group (name "dialout"))
+          (system-group (name "kmem"))
+          (system-group (name "input"))           ; input devices, from udev
+          (system-group (name "video"))
+          (system-group (name "audio"))
+          (system-group (name "netdev"))          ; used in avahi-dbus.conf
+          (system-group (name "lp"))
+          (system-group (name "disk"))
+          (system-group (name "floppy"))
+          (system-group (name "cdrom"))
+          (system-group (name "tape"))
+          (system-group (name "kvm")))))          ; for /dev/kvm
+
+(define %base-user-accounts
+  ;; List of standard user accounts.  Note that "root" is a special case, so
+  ;; it's not listed here.
+  (list (user-account
+         (name "nobody")
+         (uid 65534)
+         (group "nogroup")
+         (shell #~(string-append #$shadow "/sbin/nologin"))
+         (home-directory "/nonexistent")
+         (system? #t))))
+
+(define (default-skeletons)
+  "Return the default skeleton files for /etc/skel.  These files are copied by
+'useradd' in the home directory of newly created user accounts."
+  (define fonts.conf-content
+    ;; SXML for ~/.config/fontconfig/fonts.conf.  This works around the fact
+    ;; that Fontconfig currently does not such this directory by default,
+    ;; thereby ignoring fonts installed system-wide (FIXME).
+    `(fontconfig (dir "/run/current-system/profile/share/fonts")))
+
+  (define copy-guile-wm
+    (with-imported-modules '((guix build utils))
+      #~(begin
+          (use-modules (guix build utils))
+          (copy-file (car (find-files #$guile-wm "wm-init-sample.scm"))
+                     #$output))))
+
+  (let ((profile (plain-file "bash_profile" "\
+# Honor per-interactive-shell startup file
+if [ -f ~/.bashrc ]; then . ~/.bashrc; fi\n"))
+        (bashrc  (plain-file "bashrc" "\
+# Bash initialization for interactive non-login shells and
+# for remote shells (info \"(bash) Bash Startup Files\").
+
+# Export 'SHELL' to child processes.  Programs such as 'screen'
+# honor it and otherwise use /bin/sh.
+export SHELL
+
+if [ -n \"$SSH_CLIENT\" -a -z \"`type -P cat`\" ]
+then
+    # We are being invoked from a non-interactive SSH session
+    # (as in \"ssh host command\") but 'cat' cannot be found
+    # in $PATH.  Source /etc/profile so we get $PATH and other
+    # essential variables.
+    source /etc/profile
+fi
+
+# Adjust the prompt depending on whether we're in 'guix environment'.
+if [ -n \"$GUIX_ENVIRONMENT\" ]
+then
+    PS1='\\u@\\h \\w [env]\\$ '
+else
+    PS1='\\u@\\h \\w\\$ '
+fi
+alias ls='ls -p --color'
+alias ll='ls -l'\n"))
+        (zlogin    (plain-file "zlogin" "\
+# Honor system-wide environment variables
+source /etc/profile\n"))
+        (guile-wm  (computed-file "guile-wm" copy-guile-wm))
+        (xdefaults (plain-file "Xdefaults" "\
+XTerm*utf8: always
+XTerm*metaSendsEscape: true\n"))
+        (fonts.conf (computed-file
+                     "fonts.conf"
+                     (with-imported-modules '((guix build utils))
+                       #~(begin
+                           (use-modules (guix build utils)
+                                        (sxml simple))
+
+                           (define dir
+                             (string-append #$output
+                                            "/fontconfig"))
+
+                           (mkdir-p dir)
+                           (call-with-output-file (string-append dir
+                                                                 "/fonts.conf")
+                             (lambda (port)
+                               (sxml->xml '#$fonts.conf-content port)))))))
+        (gdbinit   (plain-file "gdbinit" "\
+# Tell GDB where to look for separate debugging files.
+set debug-file-directory ~/.guix-profile/lib/debug\n")))
+    `((".bash_profile" ,profile)
+      (".bashrc" ,bashrc)
+      (".zlogin" ,zlogin)
+      (".Xdefaults" ,xdefaults)
+      (".guile-wm" ,guile-wm)
+      (".config" ,fonts.conf)
+      (".gdbinit" ,gdbinit))))
+
+(define (skeleton-directory skeletons)
+  "Return a directory containing SKELETONS, a list of name/derivation tuples."
+  (computed-file "skel"
+                 (with-imported-modules '((guix build utils))
+                   #~(begin
+                       (use-modules (ice-9 match)
+                                    (guix build utils))
+
+                       (mkdir #$output)
+                       (chdir #$output)
+
+                       ;; Note: copy the skeletons instead of symlinking
+                       ;; them like 'file-union' does, because 'useradd'
+                       ;; would just copy the symlinks as is.
+                       (for-each (match-lambda
+                                   ((target source)
+                                    (copy-recursively source target)))
+                                 '#$skeletons)
+                       #t))))
+
+(define (assert-valid-users/groups users groups)
+  "Raise an error if USERS refer to groups not listed in GROUPS."
+  (let ((groups (list->set (map user-group-name groups))))
+    (define (validate-supplementary-group user group)
+      (unless (set-contains? groups group)
+        (raise (condition
+                (&message
+                 (message
+                  (format #f (_ "supplementary group '~a' \
+of user '~a' is undeclared")
+                          group
+                          (user-account-name user))))))))
+
+    (for-each (lambda (user)
+                (unless (set-contains? groups (user-account-group user))
+                  (raise (condition
+                          (&message
+                           (message
+                            (format #f (_ "primary group '~a' \
+of user '~a' is undeclared")
+                                    (user-account-group user)
+                                    (user-account-name user)))))))
+
+                (for-each (cut validate-supplementary-group user <>)
+                          (user-account-supplementary-groups user)))
+              users)))
+
+\f
+;;;
+;;; Service.
+;;;
+
+(define (user-group->gexp group)
+  "Turn GROUP, a <user-group> object, into a list-valued gexp suitable for
+'active-groups'."
+  #~(list #$(user-group-name group)
+          #$(user-group-password group)
+          #$(user-group-id group)
+          #$(user-group-system? group)))
+
+(define (user-account->gexp account)
+  "Turn ACCOUNT, a <user-account> object, into a list-valued gexp suitable for
+'activate-users'."
+  #~`(#$(user-account-name account)
+      #$(user-account-uid account)
+      #$(user-account-group account)
+      #$(user-account-supplementary-groups account)
+      #$(user-account-comment account)
+      #$(user-account-home-directory account)
+      ,#$(user-account-shell account)             ; this one is a gexp
+      #$(user-account-password account)
+      #$(user-account-system? account)))
+
+(define (account-activation accounts+groups)
+  "Return a gexp that activates ACCOUNTS+GROUPS, a list of <user-account> and
+<user-group> objects.  Raise an error if a user account refers to a undefined
+group."
+  (define accounts
+    (filter user-account? accounts+groups))
+
+  (define user-specs
+    (map user-account->gexp accounts))
+
+  (define groups
+    (filter user-group? accounts+groups))
+
+  (define group-specs
+    (map user-group->gexp groups))
+
+  (assert-valid-users/groups accounts groups)
+
+  ;; Add users and user groups.
+  #~(begin
+      (setenv "PATH"
+              (string-append #$(@ (gnu packages admin) shadow) "/sbin"))
+      (activate-users+groups (list #$@user-specs)
+                             (list #$@group-specs))))
+
+(define (shells-file shells)
+  "Return a file-like object that builds a shell list for use as /etc/shells
+based on SHELLS.  /etc/shells is used by xterm, polkit, and other programs."
+  (computed-file "shells"
+                 #~(begin
+                     (use-modules (srfi srfi-1))
+
+                     (define shells
+                       (delete-duplicates (list #$@shells)))
+
+                     (call-with-output-file #$output
+                       (lambda (port)
+                         (display "\
+/bin/sh
+/run/current-system/profile/bin/sh
+/run/current-system/profile/bin/bash\n" port)
+                         (for-each (lambda (shell)
+                                     (display shell port)
+                                     (newline port))
+                                   shells))))))
+(define (etc-files arguments)
+  "Filter out among ARGUMENTS things corresponding to skeletons, and return
+the /etc/skel directory for those."
+  (let ((skels (filter pair? arguments))
+        (users (filter user-account? arguments)))
+    `(("skel" ,(skeleton-directory skels))
+      ("shells" ,(shells-file (map user-account-shell users))))))
+
+(define account-service-type
+  (service-type (name 'account)
+
+                ;; Concatenate <user-account>, <user-group>, and skeleton
+                ;; lists.
+                (compose concatenate)
+                (extend append)
+
+                (extensions
+                 (list (service-extension activation-service-type
+                                          account-activation)
+                       (service-extension etc-service-type
+                                          etc-files)))))
+
+(define (account-service accounts+groups skeletons)
+  "Return a <service> that takes care of user accounts and user groups, with
+ACCOUNTS+GROUPS as its initial list of accounts and groups."
+  (service account-service-type
+           (append skeletons accounts+groups)))
 
 ;;; shadow.scm ends here