123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575 |
- ;;; GNU Guix --- Functional package management for GNU
- ;;; Copyright © 2019 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)))
- (real-name (if previous
- (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
- (days-since-epoch current-time))
- (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))
|