@@ -5,7 +5,7 @@ with pkgs.lib;
let
ids = config . ids ;
users = config . users ;
cfg = config . users ;
userOpts = { name , config , . . . }: {
@@ -28,9 +28,8 @@ let
} ;
uid = mkOption {
type = with types ; uniq ( nullOr int ) ;
default = null ;
description = " T h e a c c o u n t U I D . I f u n d e f i n e d , N i x O S w i l l s e l e c t a f r e e U I D . " ;
type = with types ; uniq int ;
description = " T h e a c c o u n t U I D . " ;
} ;
group = mkOption {
@@ -60,13 +59,21 @@ let
createHome = mkOption {
type = types . bool ;
default = false ;
description = " I f t r u e , t h e h o m e d i r e c t o r y w i l l b e c r e a t e d a u t o m a t i c a l l y . " ;
description = ''
I f t r u e , t h e h o m e d i r e c t o r y w i l l b e c r e a t e d a u t o m a t i c a l l y . I f t h i s
o p t i o n i s t r u e a n d t h e h o m e d i r e c t o r y a l r e a d y e x i s t s b u t i s n o t
o w n e d b y t h e u s e r , d i r e c t o r y o w n e r a n d g r o u p w i l l b e c h a n g e d t o
m a t c h t h e u s e r .
'' ;
} ;
useDefaultShell = mkOption {
type = types . bool ;
default = false ;
description = " I f t r u e , t h e u s e r ' s s h e l l w i l l b e s e t t o < l i t e r a l > u s e r s . d e f a u l t U s e r S h e l l < / l i t e r a l > . " ;
description = ''
I f t r u e , t h e u s e r ' s s h e l l w i l l b e s e t t o
< l i t e r a l > c f g . d e f a u l t U s e r S h e l l < / l i t e r a l > .
'' ;
} ;
password = mkOption {
@@ -78,13 +85,29 @@ let
b e c a u s e i t i s w o r l d - r e a d a b l e i n t h e N i x s t o r e . T h i s o p t i o n
s h o u l d o n l y b e u s e d f o r p u b l i c a c c o u n t s s u c h a s
< l i t e r a l > g u e s t < / l i t e r a l > .
T h e o p t i o n < l i t e r a l > p a s s w o r d < / l i t e r a l > o v e r r i d e s
< l i t e r a l > p a s s w o r d F i l e < / l i t e r a l > , i f b o t h a r e s p e c i f i e d .
I f n o n e o f t h e o p t i o n s < l i t e r a l > p a s s w o r d < / l i t e r a l > o r
< l i t e r a l > p a s s w o r d F i l e < / l i t e r a l > a r e s p e c i f i e d , t h e u s e r a c c o u n t w i l l
b e l o c k e d f o r p a s s w o r d l o g i n s . T h i s i s t h e d e f a u l t b e h a v i o r e x c e p t
f o r t h e r o o t a c c o u n t , w h i c h h a s a n e m p t y p a s s w o r d b y d e f a u l t . I f y o u
w a n t t o l o c k t h e r o o t a c c o u n t f o r p a s s w o r d l o g i n s , s e t
< l i t e r a l > u s e r s . e x t r a U s e r s . r o o t . p a s s w o r d < / l i t e r a l > t o
< l i t e r a l > n u l l < / l i t e r a l > .
'' ;
} ;
isSystemUser = mkOption {
type = types . bool ;
default = true ;
description = " I n d i c a t e s i f t h e u s e r i s a s y s t e m u s e r o r n o t . " ;
passwordFile = mkOption {
type = with types ; uniq ( nullOr string ) ;
default = null ;
description = ''
T h e p a t h t o a f i l e t h a t c o n t a i n s t h e u s e r ' s p a s s w o r d . T h e p a s s w o r d
f i l e i s r e a d o n e a c h s y s t e m a c t i v a t i o n . T h e f i l e s h o u l d c o n t a i n
e x a c t l y o n e l i n e , w h i c h s h o u l d b e t h e p a s s w o r d i n a n e n c r y p t e d f o r m
t h a t i s s u i t a b l e f o r t h e < l i t e r a l > c h p a s s w d - e < / l i t e r a l > c o m m a n d .
S e e t h e < l i t e r a l > p a s s w o r d < / l i t e r a l > f o r m o r e d e t a i l s o n h o w p a s s w o r d s
a r e a s s i g n e d .
'' ;
} ;
createUser = mkOption {
@@ -96,19 +119,11 @@ let
t h e n n o t m o d i f y a n y o f t h e b a s i c p r o p e r t i e s f o r t h e u s e r a c c o u n t .
'' ;
} ;
isAlias = mkOption {
type = types . bool ;
default = false ;
description = " I f t r u e , t h e U I D o f t h i s u s e r i s n o t r e q u i r e d t o b e u n i q u e a n d c a n t h u s a l i a s a n o t h e r u s e r . " ;
} ;
} ;
config = {
name = mkDefault name ;
uid = mkDefault ( attrByPath [ name ] n ull ids . uids ) ;
shell = mkIf config . useDefaultShell ( mkDefault users . defaultUserShell ) ;
shell = mkIf config . use DefaultShell ( mkDefa ult cfg . defaultUserShell ) ;
} ;
} ;
@@ -123,28 +138,100 @@ let
} ;
gid = mkOption {
type = with types ; uniq ( nullOr int ) ;
default = null ;
description = " T h e G I D o f t h e g r o u p . I f u n d e f i n e d , N i x O S w i l l s e l e c t a f r e e G I D . " ;
type = with types ; uniq int ;
description = " T h e G I D o f t h e g r o u p . " ;
} ;
members = mkOption {
type = with types ; listOf string ;
default = [ ] ;
description = ''
'' ;
} ;
} ;
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 " u s e r s " (
getGroup = gname :
let
p = partition ( u : u . isAlias ) (attrValues config . users . extraUsers ) ;
in concatStrings ( map serializedUser p . wrong ++ map serializedUser p . right ) ) ;
groups = mapAttrsToList ( n : g : g ) (
filterAttrs ( n : g : g . name = = gname ) cfg . extraGroups
) ;
in
if length groups = = 1 then head groups
else if groups = = [ ] then throw " G r o u p ${ gname } n o t d e f i n e d "
else throw " G r o u p ${ gname } h a s m u l t i p l e d e f i n i t i o n s " ;
getUser = uname :
let
users = mapAttrsToList ( n : u : u ) (
filterAttrs ( n : u : u . name = = uname ) cfg . extraUsers
) ;
in
if length users = = 1 then head users
else if users = = [ ] then throw " U s e r ${ uname } n o t d e f i n e d "
else throw " U s e r ${ uname } h a s m u l t i p l e d e f i n i t i o n s " ;
mkGroupEntry = gname :
let
g = getGroup gname ;
users = mapAttrsToList ( n : u : u . name ) (
filterAttrs ( n : u : elem g . name u . extraGroups ) cfg . extraUsers
) ;
in concatStringsSep " : " [
g . name " x " ( toString g . gid )
( concatStringsSep " , " ( users ++ ( filter ( u : ! ( elem u users ) ) g . members ) ) )
] ;
mkPasswdEntry = uname : let u = getUser uname ; in
concatStringsSep " : " [
u . name " x " ( toString u . uid )
( toString ( getGroup u . group ) . gid )
u . description u . home u . shell
] ;
sortOn = a : sort ( as1 : as2 : lessThan ( getAttr a as1 ) ( getAttr a as2 ) ) ;
groupFile = pkgs . writeText " g r o u p " (
concatStringsSep " \n " ( map ( g : mkGroupEntry g . name ) (
sortOn " g i d " ( attrValues cfg . extraGroups )
) )
) ;
passwdFile = pkgs . writeText " p a s s w d " (
concatStringsSep " \n " ( map ( u : mkPasswdEntry u . name ) (
sortOn " u i d " ( filter ( u : u . createUser ) ( attrValues cfg . extraUsers ) )
) )
) ;
# If mutableUsers is true, this script adds all users/groups defined in
# users.extra{Users,Groups} to /etc/{passwd,group} iff there isn't any
# existing user/group with the same name in those files.
# If mutableUsers is false, the /etc/{passwd,group} files will simply be
# replaced with the users/groups defined in the NixOS configuration.
# The merging procedure could certainly be improved, and instead of just
# keeping the lines as-is from /etc/{passwd,group} they could be combined
# in some way with the generated content from the NixOS configuration.
merger = src : pkgs . writeScript " m e r g e r " ''
# ! ${ pkgs . bash } / b i n / b a s h
P A T H = ${ pkgs . gawk } / b i n : ${ pkgs . gnugrep } / b i n : $P A T H
${ if ! cfg . mutableUsers
then '' c p ${ src } $1 . t m p ''
else '' a w k - F : ' { p r i n t " ^ " $1 " : . * " } ' $1 | e g r e p - v f - ${ src } | c a t $1 - > $1 . t m p ''
}
# s e t m t i m e t o + 1 , o t h e r w i s e c h a n g e m i g h t g o u n n o t i c e d ( v i p w / v i g r o n l y l o o k s a t m t i m e )
t o u c h - m - t $( d a t e - d @ $( ( $( s t a t - c % Y $1 ) + 1 ) ) + % Y % m % d % H % M . % S ) $1 . t m p
m v - f $1 . t m p $1
'' ;
in
@@ -154,6 +241,28 @@ in
options = {
users . mutableUsers = mkOption {
type = types . bool ;
default = true ;
description = ''
I f t r u e , y o u a r e f r e e t o a d d n e w u s e r s a n d g r o u p s t o t h e s y s t e m
w i t h t h e o r d i n a r y < l i t e r a l > u s e r a d d < / l i t e r a l > a n d
< l i t e r a l > g r o u p a d d < / l i t e r a l > c o m m a n d s . O n s y s t e m a c t i v a t i o n , t h e
e x i s t i n g c o n t e n t s o f t h e < l i t e r a l > / e t c / p a s s w d < / l i t e r a l > a n d
< l i t e r a l > / e t c / g r o u p < / l i t e r a l > f i l e s w i l l b e m e r g e d w i t h t h e
c o n t e n t s g e n e r a t e d f r o m t h e < l i t e r a l > u s e r s . e x t r a U s e r s < / l i t e r a l > a n d
< l i t e r a l > u s e r s . e x t r a G r o u p s < / l i t e r a l > o p t i o n s . I f
< l i t e r a l > m u t a b l e U s e r s < / l i t e r a l > i s f a l s e , t h e c o n t e n t s o f t h e u s e r a n d
g r o u p f i l e s w i l l s i m p l y b e r e p l a c e d o n s y s t e m a c t i v a t i o n . T h i s a l s o
h o l d s f o r t h e u s e r p a s s w o r d s ; i f t h i s o p t i o n i s f a l s e , a l l c h a n g e d
p a s s w o r d s w i l l b e r e s e t a c c o r d i n g t o t h e
< l i t e r a l > u s e r s . e x t r a U s e r s < / l i t e r a l > c o n f i g u r a t i o n o n a c t i v a t i o n . I f
t h i s o p t i o n i s t r u e , t h e i n i t i a l p a s s w o r d f o r a u s e r w i l l b e s e t
a c c o r d i n g t o < l i t e r a l > u s e r s . e x t r a U s e r s < / l i t e r a l > , b u t e x i s t i n g p a s s w o r d s
w i l l n o t b e c h a n g e d .
'' ;
} ;
users . extraUsers = mkOption {
default = { } ;
type = types . loaOf types . optionSet ;
@@ -188,20 +297,6 @@ in
options = [ groupOpts ] ;
} ;
security . initialRootPassword = mkOption {
type = types . str ;
default = " " ;
example = " ! " ;
description = ''
T h e ( h a s h e d ) p a s s w o r d f o r t h e r o o t a c c o u n t s e t o n i n i t i a l
i n s t a l l a t i o n . T h e e m p t y s t r i n g d e n o t e s t h a t r o o t c a n l o g i n
l o c a l l y w i t h o u t a p a s s w o r d ( b u t n o t v i a r e m o t e s e r v i c e s s u c h
a s S S H , o r i n d i r e c t l y v i a < c o m m a n d > s u < / c o m m a n d > o r
< c o m m a n d > s u d o < / c o m m a n d > ) . T h e s t r i n g < l i t e r a l > ! < / l i t e r a l >
p r e v e n t s r o o t f r o m l o g g i n g i n u s i n g a p a s s w o r d .
'' ;
} ;
} ;
@@ -211,144 +306,88 @@ in
users . extraUsers = {
root = {
uid = ids . uids . root ;
description = " S y s t e m a d m i n i s t r a t o r " ;
home = " / r o o t " ;
shell = config . users . defaultUserShell ;
shell = cfg . defaultUserShell ;
group = " r o o t " ;
password = mkDefault " " ;
} ;
nobody = {
uid = ids . uids . nobody ;
description = " U n p r i v i l e g e d a c c o u n t ( d o n ' t u s e ! ) " ;
group = " n o g r o u p " ;
} ;
} ;
users . extraGroups = {
root = { } ;
wheel = { } ;
disk = { } ;
kmem = { } ;
tty = { } ;
floppy = { } ;
uucp = { } ;
lp = { } ;
cdrom = { } ;
tape = { } ;
audio = { } ;
video = { } ;
dialout = { } ;
nogroup = { } ;
users = { } ;
nixbld = { } ;
utmp = { } ;
adm = { } ; # expected by journald
root . gid = ids . gids . root ;
wheel . gid = ids . gids . wheel ;
disk . gid = ids . gids . disk ;
kmem . gid = ids . gids . kmem ;
tty . gid = ids . gids . tty ;
floppy . gid = ids . gids . floppy ;
uucp . gid = ids . gids . uucp ;
lp . gid = ids . gids . lp ;
cdrom . gid = ids . gids . cdrom ;
tape . gid = ids . gids . tape ;
audio . gid = ids . gids . audio ;
video . gid = ids . gids . video ;
dialout . gid = ids . gids . dialout ;
nogroup . gid = ids . gids . nogroup ;
users . gid = ids . gids . users ;
nixbld . gid = ids . gids . nixbld ;
utmp . gid = ids . gids . utmp ;
adm . gid = ids . gids . adm ;
} ;
system . activationScripts . rootPasswd = stringAfter [ " e t c " ]
''
# I f t h e r e i s n o p a s s w o r d f i l e y e t , c r e a t e a r o o t a c c o u n t w i t h a n
# e m p t y p a s s w o r d .
i f ! t e s t - e / e t c / p a s s w d ; t h e n
r o o t H o m e = / r o o t
t o u c h / e t c / p a s s w d ; c h m o d 0 6 4 4 / e t c / p a s s w d
t o u c h / e t c / g r o u p ; c h m o d 0 6 4 4 / e t c / g r o u p
t o u c h / e t c / s h a d o w ; c h m o d 0 6 0 0 / e t c / s h a d o w
# C a n ' t u s e u s e r a d d , s i n c e i t c o m p l a i n s t h a t i t d o e s n ' t k n o w u s
# ( b o o t s t r a p p r o b l e m ! ) .
e c h o " r o o t : x : 0 : 0 : S y s t e m a d m i n i s t r a t o r : $r o o t H o m e : ${ config . users . defaultUserShell } " > > / e t c / p a s s w d
e c h o " r o o t : ${ config . security . initialRootPassword } : : : : : : : " > > / e t c / s h a d o w
f i
system . activationScripts . users =
let
mkhomeUsers = filterAttrs ( n : u : u . createHome ) cfg . extraUsers ;
setpwUsers = filterAttrs ( n : u : u . createUser ) cfg . extraUsers ;
setpw = n : u : ''
s e t p w = y e s
${ optionalString cfg . mutableUsers ''
t e s t " $( g e t e n t s h a d o w ' ${ u . name } ' | c u t - d : - f 2 ) " ! = " x " & & s e t p w = n o
'' }
i f [ " $ se t p w " = = " y e s " ] ; t h e n
${ if u . password = = " "
then " p a s s w d - d ' ${ u . name } ' & > / d e v / n u l l "
else if ( isNull u . password && isNull u . passwordFile )
then " p a s s w d - l ' ${ u . name } ' & > / d e v / n u l l "
else if ! ( isNull u . password )
then ''
e c h o " ${ u . name } : ${ u . password } " | ${ pkgs . shadow } / s b i n / c h p a s s w d ''
else ''
e c h o - n " ${ u . name } : " | c a t - " ${ u . passwordFile } " | \
${ pkgs . shadow } / s b i n / c h p a s s w d - e
''
}
f i
'' ;
mkhome = n : u :
let
uid = toString u . uid ;
gid = toString ( ( getGroup u . group ) . gid ) ;
h = u . home ;
in ''
t e s t - a " ${ h } " | | m k d i r - p " ${ h } " | | t r u e
t e s t " $( s t a t - c % u " ${ h } " ) " = ${ uid } | | c h o w n ${ uid } " ${ h } " | | t r u e
t e s t " $( s t a t - c % g " ${ h } " ) " = ${ gid } | | c h g r p ${ gid } " ${ h } " | | t r u e
'' ;
in stringAfter [ " e t c " ] ''
t o u c h / e t c / g r o u p
t o u c h / e t c / p a s s w d
V I S U A L = ${ merger groupFile } ${ pkgs . shadow } / s b i n / v i g r & > / d e v / n u l l
V I S U A L = ${ merger passwdFile } ${ pkgs . shadow } / s b i n / v i p w & > / d e v / n u l l
${ pkgs . shadow } / s b i n / g r p c o n v
${ pkgs . shadow } / s b i n / p w c o n v
${ concatStrings ( mapAttrsToList mkhome mkhomeUsers ) }
${ concatStrings ( mapAttrsToList setpw setpwUsers ) }
'' ;
# Print a reminder for users to set a root password.
environment . interactiveShellInit =
''
i f [ " $U I D " = 0 ] ; t h e n
r e a d _ l < / e t c / s h a d o w
i f [ " ''$ { _ l : 0 : 6 } " = r o o t : : ] ; t h e n
c a t > & 2 < < E O F
[ 1 ; 3 1 m W a r n i n g : [ 0 m Y o u r r o o t a c c o u n t h a s a n u l l p a s s w o r d , a l l o w i n g l o c a l u s e r s
t o l o g i n a s r o o t . P l e a s e s e t a n o n - n u l l p a s s w o r d u s i n g \ ` p a s s w d ' , o r
d i s a b l e p a s s w o r d - b a s e d r o o t l o g i n s u s i n g \ ` p a s s w d - l ' .
E O F
f i
u n s e t _ l
f i
'' ;
system . activationScripts . users = stringAfter [ " g r o u p s " ]
''
e c h o " u p d a t i n g u s e r s . . . "
c a t ${ usersFile } | w h i l e t r u e ; d o
r e a d n a m e | | b r e a k
r e a d d e s c r i p t i o n
r e a d u i d
r e a d g r o u p
r e a d e x t r a G r o u p s
r e a d h o m e
r e a d s h e l l
r e a d c r e a t e H o m e
r e a d p a s s w o r d
r e a d i s S y s t e m U s e r
r e a d c r e a t e U s e r
r e a d i s A l i a s
i f [ - z " $c r e a t e U s e r " ] ; t h e n
c o n t i n u e
f i
i f ! c u r E n t = $( g e t e n t p a s s w d " $n a m e " ) ; t h e n
u s e r a d d ''$ { i s S y s t e m U s e r : + - - s y s t e m } \
- - c o m m e n t " $d e s c r i p t i o n " \
''$ { u i d : + - - u i d $u i d } \
- - g i d " $g r o u p " \
- - g r o u p s " $e x t r a G r o u p s " \
- - h o m e " $h o m e " \
- - s h e l l " $s h e l l " \
''$ { c r e a t e H o m e : + - - c r e a t e - h o m e } \
''$ { i s A l i a s : + - - n o n - u n i q u e } \
" $n a m e "
i f t e s t " ''$ { p a s s w o r d : 0 : 1 } " = ' X ' ; t h e n
( e c h o " ''$ { p a s s w o r d : 1 } " ; e c h o " ''$ { p a s s w o r d : 1 } " ) | ${ pkgs . shadow } / b i n / p a s s w d " $n a m e "
f i
e l s e
# e c h o " u p d a t i n g u s e r $n a m e . . . "
o l d I F S = " $I F S " ; I F S = : ; s e t - - $c u r E n t ; I F S = " $o l d I F S "
p r e v U i d = $3
p r e v H o m e = $6
# D o n ' t c h a n g e t h e h o m e d i r e c t o r y i f i t ' s t h e s a m e t o p r e v e n t
# u n n e c e s s a r y w a r n i n g s a b o u t l o g g e d i n u s e r s .
i f t e s t " $p r e v H o m e " = " $h o m e " ; t h e n u n s e t h o m e ; f i
u s e r m o d \
- - c o m m e n t " $d e s c r i p t i o n " \
- - g i d " $g r o u p " \
- - g r o u p s " $e x t r a G r o u p s " \
''$ { h o m e : + - - h o m e " $h o m e " } \
- - s h e l l " $s h e l l " \
" $n a m e "
f i
d o n e
'' ;
system . activationScripts . groups = stringAfter [ " r o o t P a s s w d " " b i n s h " " e t c " " v a r " ]
''
e c h o " u p d a t i n g g r o u p s . . . "
c r e a t e G r o u p ( ) {
n a m e = " $1 "
g i d = " $2 "
i f ! c u r E n t = $( g e t e n t g r o u p " $n a m e " ) ; t h e n
g r o u p a d d - - s y s t e m \
''$ { g i d : + - - g i d $g i d } \
" $n a m e "
f i
}
${ flip concatMapStrings ( attrValues config . users . extraGroups ) ( g : ''
c r e a t e G r o u p ' ${ g . name } ' ' ${ toString g . gid } '
'' ) }
'' ;
# for backwards compatibility
system . activationScripts . groups = stringAfter [ " u s e r s " ] " " ;
} ;