;;; 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