586 lines
		
	
	
	
		
			22 KiB
		
	
	
	
		
			Scheme
		
	
	
	
	
	
			
		
		
	
	
			586 lines
		
	
	
	
		
			22 KiB
		
	
	
	
		
			Scheme
		
	
	
	
	
	
| ;;; GNU Guix --- Functional package management for GNU
 | ||
| ;;; Copyright © 2019, 2021, 2023 Ludovic Courtès <ludo@gnu.org>
 | ||
| ;;;
 | ||
| ;;; This file is part of GNU Guix.
 | ||
| ;;;
 | ||
| ;;; GNU Guix is free software; you can redistribute it and/or modify it
 | ||
| ;;; under the terms of the GNU General Public License as published by
 | ||
| ;;; the Free Software Foundation; either version 3 of the License, or (at
 | ||
| ;;; your option) any later version.
 | ||
| ;;;
 | ||
| ;;; GNU Guix is distributed in the hope that it will be useful, but
 | ||
| ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||
| ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||
| ;;; GNU General Public License for more details.
 | ||
| ;;;
 | ||
| ;;; You should have received a copy of the GNU General Public License
 | ||
| ;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
 | ||
| 
 | ||
| (define-module (gnu build accounts)
 | ||
|   #:use-module (guix records)
 | ||
|   #:use-module (guix combinators)
 | ||
|   #:use-module (gnu system accounts)
 | ||
|   #:use-module (srfi srfi-1)
 | ||
|   #:use-module (srfi srfi-11)
 | ||
|   #:use-module (srfi srfi-19)
 | ||
|   #:use-module (srfi srfi-26)
 | ||
|   #:use-module (ice-9 match)
 | ||
|   #:use-module (ice-9 vlist)
 | ||
|   #:use-module (ice-9 rdelim)
 | ||
|   #:export (password-entry
 | ||
|             password-entry?
 | ||
|             password-entry-name
 | ||
|             password-entry-uid
 | ||
|             password-entry-gid
 | ||
|             password-entry-real-name
 | ||
|             password-entry-directory
 | ||
|             password-entry-shell
 | ||
| 
 | ||
|             shadow-entry
 | ||
|             shadow-entry?
 | ||
|             shadow-entry-name
 | ||
|             shadow-entry-minimum-change-period
 | ||
|             shadow-entry-maximum-change-period
 | ||
|             shadow-entry-change-warning-time
 | ||
|             shadow-entry-maximum-inactivity
 | ||
|             shadow-entry-expiration
 | ||
| 
 | ||
|             group-entry
 | ||
|             group-entry?
 | ||
|             group-entry-name
 | ||
|             group-entry-gid
 | ||
|             group-entry-members
 | ||
| 
 | ||
|             %password-lock-file
 | ||
|             write-group
 | ||
|             write-passwd
 | ||
|             write-shadow
 | ||
|             read-group
 | ||
|             read-passwd
 | ||
|             read-shadow
 | ||
| 
 | ||
|             %id-min
 | ||
|             %id-max
 | ||
|             %system-id-min
 | ||
|             %system-id-max
 | ||
| 
 | ||
|             user+group-databases))
 | ||
| 
 | ||
| ;;; Commentary:
 | ||
| ;;;
 | ||
| ;;; This modules provides functionality equivalent to the C library's
 | ||
| ;;; <shadow.h>, <pwd.h>, and <grp.h> routines, as well as a subset of the
 | ||
| ;;; functionality of the Shadow command-line tools.  It can parse and write
 | ||
| ;;; /etc/passwd, /etc/shadow, and /etc/group.  It can also take care of UID
 | ||
| ;;; and GID allocation in a way similar to what 'useradd' does.
 | ||
| ;;;
 | ||
| ;;; The benefit is twofold: less code is involved, and the ID allocation
 | ||
| ;;; strategy and state preservation is made explicit.
 | ||
| ;;;
 | ||
| ;;; Code:
 | ||
| 
 | ||
| 
 | ||
| ;;;
 | ||
| ;;; Machinery to define user and group databases.
 | ||
| ;;;
 | ||
| 
 | ||
| (define-syntax serialize-field
 | ||
|   (syntax-rules (serialization)
 | ||
|     ((_ entry (field get (serialization ->string string->) _ ...))
 | ||
|      (->string (get entry)))
 | ||
|     ((_ entry (field get _ ...))
 | ||
|      (get entry))))
 | ||
| 
 | ||
| (define-syntax deserialize-field
 | ||
|   (syntax-rules (serialization)
 | ||
|     ((_ str (field get (serialization ->string string->) _ ...))
 | ||
|      (string-> str))
 | ||
|     ((_ str (field get _ ...))
 | ||
|      str)))
 | ||
| 
 | ||
| (define-syntax let/fields
 | ||
|   (syntax-rules ()
 | ||
|     ((_ (((name get attributes ...) rest ...) lst) body ...)
 | ||
|      (let ((l lst))
 | ||
|        (let ((name (deserialize-field (car l)
 | ||
|                                       (name get attributes ...))))
 | ||
|          (let/fields ((rest ...) (cdr l)) body ...))))
 | ||
|     ((_ (() lst) body ...)
 | ||
|      (begin body ...))))
 | ||
| 
 | ||
| (define-syntax define-database-entry
 | ||
|   (syntax-rules (serialization)
 | ||
|     "Define a record data type, as per 'define-record-type*', with additional
 | ||
| information on how to serialize and deserialize the whole database as well as
 | ||
| each field."
 | ||
|     ((_ <record> record make-record record?
 | ||
|         (serialization separator entry->string string->entry)
 | ||
|         fields ...)
 | ||
|      (let-syntax ((field-name
 | ||
|                    (syntax-rules ()
 | ||
|                      ((_ (name _ (... ...))) name))))
 | ||
|        (define-record-type* <record> record make-record
 | ||
|          record?
 | ||
|          fields ...)
 | ||
| 
 | ||
|        (define (entry->string entry)
 | ||
|          (string-join (list (serialize-field entry fields) ...)
 | ||
|                       (string separator)))
 | ||
| 
 | ||
|        (define (string->entry str)
 | ||
|          (let/fields ((fields ...) (string-split str #\:))
 | ||
|                      (make-record (field-name fields) ...)))))))
 | ||
| 
 | ||
| 
 | ||
| (define number->string*
 | ||
|   (match-lambda
 | ||
|     ((? number? number) (number->string number))
 | ||
|     (_ "")))
 | ||
| 
 | ||
| (define (false-if-string=? false-string)
 | ||
|   (lambda (str)
 | ||
|     (if (string=? str false-string)
 | ||
|         #f
 | ||
|         str)))
 | ||
| 
 | ||
| (define (string-if-false str)
 | ||
|   (lambda (obj)
 | ||
|     (if (not obj) str obj)))
 | ||
| 
 | ||
| (define (comma-separated->list str)
 | ||
|   (string-tokenize str (char-set-complement (char-set #\,))))
 | ||
| 
 | ||
| (define (list->comma-separated lst)
 | ||
|   (string-join lst ","))
 | ||
| 
 | ||
| 
 | ||
| ;;;
 | ||
| ;;; Database definitions.
 | ||
| ;;;
 | ||
| 
 | ||
| (define-database-entry <password-entry>           ;<pwd.h>
 | ||
|   password-entry make-password-entry
 | ||
|   password-entry?
 | ||
|   (serialization #\: password-entry->string string->password-entry)
 | ||
| 
 | ||
|   (name       password-entry-name)
 | ||
|   (password   password-entry-password
 | ||
|               (serialization (const "x") (const #f))
 | ||
|               (default "x"))
 | ||
|   (uid        password-entry-uid
 | ||
|               (serialization number->string string->number))
 | ||
|   (gid        password-entry-gid
 | ||
|               (serialization number->string string->number))
 | ||
|   (real-name  password-entry-real-name
 | ||
|               (default ""))
 | ||
|   (directory  password-entry-directory)
 | ||
|   (shell      password-entry-shell
 | ||
|               (default "/bin/sh")))
 | ||
| 
 | ||
| (define-database-entry <shadow-entry>             ;<shadow.h>
 | ||
|   shadow-entry make-shadow-entry
 | ||
|   shadow-entry?
 | ||
|   (serialization #\: shadow-entry->string string->shadow-entry)
 | ||
| 
 | ||
|   (name                  shadow-entry-name)       ;string
 | ||
|   (password              shadow-entry-password    ;string | #f
 | ||
|                          (serialization (string-if-false "!")
 | ||
|                                         (false-if-string=? "!"))
 | ||
|                          (default #f))
 | ||
|   (last-change           shadow-entry-last-change ;days since 1970-01-01
 | ||
|                          (serialization number->string* string->number)
 | ||
|                          (default 0))
 | ||
|   (minimum-change-period shadow-entry-minimum-change-period
 | ||
|                          (serialization number->string* string->number)
 | ||
|                          (default #f))            ;days | #f
 | ||
|   (maximum-change-period shadow-entry-maximum-change-period
 | ||
|                          (serialization number->string* string->number)
 | ||
|                          (default #f))            ;days | #f
 | ||
|   (change-warning-time   shadow-entry-change-warning-time
 | ||
|                          (serialization number->string* string->number)
 | ||
|                          (default #f))            ;days | #f
 | ||
|   (maximum-inactivity    shadow-entry-maximum-inactivity
 | ||
|                          (serialization number->string* string->number)
 | ||
|                          (default #f))             ;days | #f
 | ||
|   (expiration            shadow-entry-expiration
 | ||
|                          (serialization number->string* string->number)
 | ||
|                          (default #f))            ;days since 1970-01-01 | #f
 | ||
|   (flags                 shadow-entry-flags       ;"reserved"
 | ||
|                          (serialization number->string* string->number)
 | ||
|                          (default #f)))
 | ||
| 
 | ||
| (define-database-entry <group-entry>              ;<grp.h>
 | ||
|   group-entry make-group-entry
 | ||
|   group-entry?
 | ||
|   (serialization #\: group-entry->string string->group-entry)
 | ||
| 
 | ||
|   (name            group-entry-name)
 | ||
|   (password        group-entry-password
 | ||
|                    (serialization (string-if-false "x")
 | ||
|                                   (false-if-string=? "x"))
 | ||
|                    (default #f))
 | ||
|   (gid             group-entry-gid
 | ||
|                    (serialization number->string string->number))
 | ||
|   (members         group-entry-members
 | ||
|                    (serialization list->comma-separated comma-separated->list)
 | ||
|                    (default '())))
 | ||
| 
 | ||
| (define %password-lock-file
 | ||
|   ;; The password database lock file used by libc's 'lckpwdf'.  Users should
 | ||
|   ;; grab this lock with 'with-file-lock' when they access the databases.
 | ||
|   "/etc/.pwd.lock")
 | ||
| 
 | ||
| (define (database-writer file mode entry->string)
 | ||
|   (lambda* (entries #:optional (file-or-port file))
 | ||
|     "Write ENTRIES to FILE-OR-PORT.  When FILE-OR-PORT is a file name, write
 | ||
| to it atomically and set the appropriate permissions."
 | ||
|     (define (write-entries port)
 | ||
|       (for-each (lambda (entry)
 | ||
|                   (display (entry->string entry) port)
 | ||
|                   (newline port))
 | ||
|                 (delete-duplicates entries)))
 | ||
| 
 | ||
|     (if (port? file-or-port)
 | ||
|         (write-entries file-or-port)
 | ||
|         (let* ((template (string-append file-or-port ".XXXXXX"))
 | ||
|                (port     (mkstemp! template)))
 | ||
|           (dynamic-wind
 | ||
|             (const #t)
 | ||
|             (lambda ()
 | ||
|               (chmod port mode)
 | ||
|               (write-entries port)
 | ||
| 
 | ||
|               (fsync port)
 | ||
|               (close-port port)
 | ||
|               (rename-file template file-or-port))
 | ||
|             (lambda ()
 | ||
|               (unless (port-closed? port)
 | ||
|                 (close-port port))
 | ||
|               (when (file-exists? template)
 | ||
|                 (delete-file template))))))))
 | ||
| 
 | ||
| (define write-passwd
 | ||
|   (database-writer "/etc/passwd" #o644 password-entry->string))
 | ||
| (define write-shadow
 | ||
|   (database-writer "/etc/shadow" #o600 shadow-entry->string))
 | ||
| (define write-group
 | ||
|   (database-writer "/etc/group" #o644 group-entry->string))
 | ||
| 
 | ||
| (define (database-reader file string->entry)
 | ||
|   (lambda* (#:optional (file-or-port file))
 | ||
|     (define (read-entries port)
 | ||
|       (let loop ((entries '()))
 | ||
|         (match (read-line port)
 | ||
|           ((? eof-object?)
 | ||
|            (reverse entries))
 | ||
|           (line
 | ||
|            (loop (cons (string->entry line) entries))))))
 | ||
| 
 | ||
|     (if (port? file-or-port)
 | ||
|         (read-entries file-or-port)
 | ||
|         (call-with-input-file file-or-port
 | ||
|           read-entries))))
 | ||
| 
 | ||
| (define read-passwd
 | ||
|   (database-reader "/etc/passwd" string->password-entry))
 | ||
| (define read-shadow
 | ||
|   (database-reader "/etc/shadow" string->shadow-entry))
 | ||
| (define read-group
 | ||
|   (database-reader "/etc/group" string->group-entry))
 | ||
| 
 | ||
| 
 | ||
| ;;;
 | ||
| ;;; Building databases.
 | ||
| ;;;
 | ||
| 
 | ||
| (define-record-type* <allocation>
 | ||
|   allocation make-allocation
 | ||
|   allocation?
 | ||
|   (ids            allocation-ids (default vlist-null))
 | ||
|   (next-id        allocation-next-id (default %id-min))
 | ||
|   (next-system-id allocation-next-system-id (default %system-id-max)))
 | ||
| 
 | ||
| ;; Trick to avoid name clashes...
 | ||
| (define-syntax %allocation (identifier-syntax allocation))
 | ||
| 
 | ||
| ;; Minimum and maximum UIDs and GIDs (from find_new_uid.c and find_new_gid.c
 | ||
| ;; in Shadow.)
 | ||
| (define %id-min 1000)
 | ||
| (define %id-max 60000)
 | ||
| 
 | ||
| (define %system-id-min 100)
 | ||
| (define %system-id-max 999)
 | ||
| 
 | ||
| (define (system-id? id)
 | ||
|   (and (> id %system-id-min)
 | ||
|        (<= id %system-id-max)))
 | ||
| 
 | ||
| (define (user-id? id)
 | ||
|   (and (>= id %id-min)
 | ||
|        (< id %id-max)))
 | ||
| 
 | ||
| (define* (allocate-id assignment #:key system?)
 | ||
|   "Return two values: a newly allocated ID, and an updated <allocation> record
 | ||
| based on ASSIGNMENT.  If SYSTEM? is true, return a system ID."
 | ||
|   (define next
 | ||
|     ;; Return the next available ID, looping if necessary.
 | ||
|     (if system?
 | ||
|         (lambda (id)
 | ||
|           (let ((next-id (- id 1)))
 | ||
|             (if (< next-id %system-id-min)
 | ||
|                 %system-id-max
 | ||
|                 next-id)))
 | ||
|         (lambda (id)
 | ||
|           (let ((next-id (+ id 1)))
 | ||
|             (if (>= next-id %id-max)
 | ||
|                 %id-min
 | ||
|                 next-id)))))
 | ||
| 
 | ||
|   (let loop ((id (if system?
 | ||
|                      (allocation-next-system-id assignment)
 | ||
|                      (allocation-next-id assignment))))
 | ||
|     (if (vhash-assv id (allocation-ids assignment))
 | ||
|         (loop (next id))
 | ||
|         (let ((taken (vhash-consv id #t (allocation-ids assignment))))
 | ||
|           (values (if system?
 | ||
|                       (allocation (inherit assignment)
 | ||
|                                   (next-system-id (next id))
 | ||
|                                   (ids taken))
 | ||
|                       (allocation (inherit assignment)
 | ||
|                                   (next-id (next id))
 | ||
|                                   (ids taken)))
 | ||
|                   id)))))
 | ||
| 
 | ||
| (define* (reserve-ids allocation ids #:key (skip? #t))
 | ||
|   "Mark the numbers listed in IDS as reserved in ALLOCATION.  When SKIP? is
 | ||
| true, start allocation after the highest (or lowest, depending on whether it's
 | ||
| a system ID allocation) number among IDS."
 | ||
|   (%allocation
 | ||
|    (inherit allocation)
 | ||
|    (next-id (if skip?
 | ||
|                 (+ (reduce max
 | ||
|                            (- (allocation-next-id allocation) 1)
 | ||
|                            (filter user-id? ids))
 | ||
|                    1)
 | ||
|                 (allocation-next-id allocation)))
 | ||
|    (next-system-id
 | ||
|     (if skip?
 | ||
|         (- (reduce min
 | ||
|                    (+ 1 (allocation-next-system-id allocation))
 | ||
|                    (filter system-id? ids))
 | ||
|            1)
 | ||
|         (allocation-next-system-id allocation)))
 | ||
|    (ids (fold (cut vhash-consv <> #t <>)
 | ||
|               (allocation-ids allocation)
 | ||
|               ids))))
 | ||
| 
 | ||
| (define (allocated? allocation id)
 | ||
|   "Return true if ID is already allocated as part of ALLOCATION."
 | ||
|   (->bool (vhash-assv id (allocation-ids allocation))))
 | ||
| 
 | ||
| (define (lookup-procedure lst key)
 | ||
|   "Return a lookup procedure for the elements of LST, calling KEY to obtain
 | ||
| the key of each element."
 | ||
|   (let ((table (fold (lambda (obj table)
 | ||
|                        (vhash-cons (key obj) obj table))
 | ||
|                      vlist-null
 | ||
|                      lst)))
 | ||
|     (lambda (key)
 | ||
|       (match (vhash-assoc key table)
 | ||
|         (#f #f)
 | ||
|         ((_ . value) value)))))
 | ||
| 
 | ||
| (define* (allocate-groups groups members
 | ||
|                           #:optional (current-groups '()))
 | ||
|   "Return a list of group entries for GROUPS, a list of <user-group>.  Members
 | ||
| for each group are taken from MEMBERS, a vhash that maps group names to member
 | ||
| names.  GIDs and passwords found in CURRENT-GROUPS, a list of group entries,
 | ||
| are reused."
 | ||
|   (define gids
 | ||
|     ;; Mark all the currently-used GIDs and the explicitly requested GIDs as
 | ||
|     ;; reserved.
 | ||
|     (reserve-ids (reserve-ids (allocation)
 | ||
|                               (map group-entry-gid current-groups))
 | ||
|                  (filter-map user-group-id groups)
 | ||
|                  #:skip? #f))
 | ||
| 
 | ||
|   (define previous-entry
 | ||
|     (lookup-procedure current-groups group-entry-name))
 | ||
| 
 | ||
|   (reverse
 | ||
|    (fold2 (lambda (group result allocation)
 | ||
|             (let ((name         (user-group-name group))
 | ||
|                   (password     (user-group-password group))
 | ||
|                   (requested-id (user-group-id group))
 | ||
|                   (system?      (user-group-system? group)))
 | ||
|               (let*-values (((previous)
 | ||
|                              (previous-entry name))
 | ||
|                             ((allocation id)
 | ||
|                              (cond
 | ||
|                               ((number? requested-id)
 | ||
|                                (values (reserve-ids allocation
 | ||
|                                                     (list requested-id))
 | ||
|                                        requested-id))
 | ||
|                               (previous
 | ||
|                                (values allocation
 | ||
|                                        (group-entry-gid previous)))
 | ||
|                               (else
 | ||
|                                (allocate-id allocation
 | ||
|                                             #:system? system?)))))
 | ||
|                 (values (cons (group-entry
 | ||
|                                (name name)
 | ||
|                                (password
 | ||
|                                 (if previous
 | ||
|                                     (group-entry-password previous)
 | ||
|                                     password))
 | ||
|                                (gid id)
 | ||
|                                (members (vhash-fold* cons '() name members)))
 | ||
|                               result)
 | ||
|                         allocation))))
 | ||
|           '()
 | ||
|           gids
 | ||
|           groups)))
 | ||
| 
 | ||
| (define* (allocate-passwd users groups #:optional (current-passwd '()))
 | ||
|   "Return a list of password entries for USERS, a list of <user-account>.
 | ||
| Take GIDs from GROUPS, a list of group entries.  Reuse UIDs from
 | ||
| CURRENT-PASSWD, a list of password entries, when possible; otherwise allocate
 | ||
| new UIDs."
 | ||
|   (define uids
 | ||
|     (reserve-ids (reserve-ids (allocation)
 | ||
|                               (map password-entry-uid current-passwd))
 | ||
|                  (filter-map user-account-uid users)
 | ||
|                  #:skip? #f))
 | ||
| 
 | ||
|   (define previous-entry
 | ||
|     (lookup-procedure current-passwd password-entry-name))
 | ||
| 
 | ||
|   (define (group-id name)
 | ||
|     (or (any (lambda (entry)
 | ||
|                (and (string=? (group-entry-name entry) name)
 | ||
|                     (group-entry-gid entry)))
 | ||
|              groups)
 | ||
|         (error "group not found" name)))
 | ||
| 
 | ||
|   (reverse
 | ||
|    (fold2 (lambda (user result allocation)
 | ||
|             (let ((name         (user-account-name user))
 | ||
|                   (requested-id (user-account-uid user))
 | ||
|                   (group        (user-account-group user))
 | ||
|                   (real-name    (user-account-comment user))
 | ||
|                   (directory    (user-account-home-directory user))
 | ||
|                   (shell        (user-account-shell user))
 | ||
|                   (system?      (user-account-system? user)))
 | ||
|               (let*-values (((previous)
 | ||
|                              (previous-entry name))
 | ||
|                             ((allocation id)
 | ||
|                              (cond
 | ||
|                               ((number? requested-id)
 | ||
|                                (values (reserve-ids allocation
 | ||
|                                                     (list requested-id))
 | ||
|                                        requested-id))
 | ||
|                               (previous
 | ||
|                                (values allocation
 | ||
|                                        (password-entry-uid previous)))
 | ||
|                               (else
 | ||
|                                (allocate-id allocation
 | ||
|                                             #:system? system?)))))
 | ||
|                 (values (cons (password-entry
 | ||
|                                (name name)
 | ||
|                                (uid id)
 | ||
|                                (directory directory)
 | ||
|                                (gid (if (number? group) group (group-id group)))
 | ||
| 
 | ||
|                                ;; Users might change their name to something
 | ||
|                                ;; other than what the sysadmin chose, with
 | ||
|                                ;; 'chfn'.  Thus consider it "stateful".
 | ||
|                                (real-name (if (and previous (not system?))
 | ||
|                                               (password-entry-real-name previous)
 | ||
|                                               real-name))
 | ||
| 
 | ||
|                                ;; Do not reuse the shell of PREVIOUS since (1)
 | ||
|                                ;; that could lead to confusion, and (2) the
 | ||
|                                ;; shell might have been GC'd.  See
 | ||
|                                ;; <https://lists.gnu.org/archive/html/guix-devel/2019-04/msg00478.html>.
 | ||
|                                (shell shell))
 | ||
|                               result)
 | ||
|                         allocation))))
 | ||
|           '()
 | ||
|           uids
 | ||
|           users)))
 | ||
| 
 | ||
| (define* (days-since-epoch #:optional (current-time current-time))
 | ||
|   "Return the number of days elapsed since the 1st of January, 1970."
 | ||
|   (let* ((now   (current-time time-utc))
 | ||
|          (epoch (make-time time-utc 0 0))
 | ||
|          (diff  (time-difference now epoch)))
 | ||
|     (quotient (time-second diff) (* 24 3600))))
 | ||
| 
 | ||
| (define* (passwd->shadow users passwd #:optional (current-shadow '())
 | ||
|                          #:key (current-time current-time))
 | ||
|   "Return a list of shadow entries for the password entries listed in PASSWD.
 | ||
| Reuse shadow entries from CURRENT-SHADOW when they exist, and take the initial
 | ||
| password from USERS."
 | ||
|   (define previous-entry
 | ||
|     (lookup-procedure current-shadow shadow-entry-name))
 | ||
| 
 | ||
|   (define now
 | ||
|     ;; On machines without a real-time clock (typically Arm SBCs), the system
 | ||
|     ;; clock may be at 1970-01-01 while booting, which would lead us to define
 | ||
|     ;; NOW as zero.
 | ||
|     ;;
 | ||
|     ;; However, the 'isexpired' function in Shadow interprets the combination
 | ||
|     ;; uninitialized password + last-change = 0 as "The password has expired,
 | ||
|     ;; it must be changed", which prevents logins altogether.  To avoid that,
 | ||
|     ;; never set 'last-change' to zero.
 | ||
|     (max (days-since-epoch current-time) 1))
 | ||
| 
 | ||
|   (map (lambda (user passwd)
 | ||
|          (or (previous-entry (password-entry-name passwd))
 | ||
|              (shadow-entry (name (password-entry-name passwd))
 | ||
|                            (password (user-account-password user))
 | ||
|                            (last-change now))))
 | ||
|        users passwd))
 | ||
| 
 | ||
| (define (empty-if-not-found thunk)
 | ||
|   "Call THUNK and return the empty list if that throws to ENOENT."
 | ||
|   (catch 'system-error
 | ||
|     thunk
 | ||
|     (lambda args
 | ||
|       (if (= ENOENT (system-error-errno args))
 | ||
|           '()
 | ||
|           (apply throw args)))))
 | ||
| 
 | ||
| (define* (user+group-databases users groups
 | ||
|                                #:key
 | ||
|                                (current-passwd
 | ||
|                                 (empty-if-not-found read-passwd))
 | ||
|                                (current-groups
 | ||
|                                 (empty-if-not-found read-group))
 | ||
|                                (current-shadow
 | ||
|                                 (empty-if-not-found read-shadow))
 | ||
|                                (current-time current-time))
 | ||
|   "Return three values: the list of group entries, the list of password
 | ||
| entries, and the list of shadow entries corresponding to USERS and GROUPS.
 | ||
| Preserve stateful bits from CURRENT-PASSWD, CURRENT-GROUPS, and
 | ||
| CURRENT-SHADOW: UIDs, GIDs, passwords, user shells, etc."
 | ||
|   (define members
 | ||
|     ;; Map group name to user names.
 | ||
|     (fold (lambda (user members)
 | ||
|             (fold (cute vhash-cons <> (user-account-name user) <>)
 | ||
|                   members
 | ||
|                   (user-account-supplementary-groups user)))
 | ||
|           vlist-null
 | ||
|           users))
 | ||
| 
 | ||
|   (define group-entries
 | ||
|     (allocate-groups groups members current-groups))
 | ||
| 
 | ||
|   (define passwd-entries
 | ||
|     (allocate-passwd users group-entries current-passwd))
 | ||
| 
 | ||
|   (define shadow-entries
 | ||
|     (passwd->shadow users passwd-entries current-shadow
 | ||
|                     #:current-time current-time))
 | ||
| 
 | ||
|   (values group-entries passwd-entries shadow-entries))
 |