nixos: Rewrite directory creation for saner default permissions

Construct directory items for all parent directories of the user
specified files and directories, assigning better default permissions
and ownership to each and removing this responsibility from the
create-directories script.

This means that all parent directories of root directories will now
have the default permissions and ownership, not inherit them from the
child. User directories are assigned default user ownership. The home
directory itself is handled specially to make sure it is owned by the
user, not readable by anyone else and its parent gets default root
ownership.

To illustrate this with an example, here is a directory specification
and the ownership and permissions that could potentially be assigned
to the parent directories, given none of them yet exist in persistent
storage:

environment.persistence."/persistent" = {
  users.talyz = {
    directories = [
      { directory = ".local/share/secret"; mode = "0500"; }
    ];
  };
};

Before:
/home                            talyz:talyz   0500
/home/talyz                      talyz:talyz   0500
/home/talyz/.local               talyz:talyz   0500
/home/talyz/.local/share         talyz:talyz   0500
/home/talyz/.local/share/secret  talyz:talyz   0500

After:
/home                            root:root     0755
/home/talyz                      talyz:talyz   0700
/home/talyz/.local               talyz:talyz   0755
/home/talyz/.local/share         talyz:talyz   0755
/home/talyz/.local/share/secret  talyz:talyz   0500
This commit is contained in:
talyz 2022-11-13 18:18:15 +01:00
parent d30c421e4e
commit b4160ba71d
No known key found for this signature in database
GPG key ID: 2DED2151F4671A2B
4 changed files with 134 additions and 44 deletions

View file

@ -109,10 +109,10 @@
- ~mode~, the permissions to set for the directory. If the
directory doesn't already exist in persistent storage, it will
be created with this mode. Can be either an octal mode
(e.g. ~0700~) or a symbolic mode (e.g. ~u=rwx,g=,o=~). This also
applies to any parent directories which don't yet exist.
Changing this once the directory has been created has no
effect.
(e.g. ~0700~) or a symbolic mode (e.g. ~u=rwx,g=,o=~). Parent
directories that don't yet exist are created with default
permissions. Changing this once the directory has been created
has no effect.
- ~files~ are all files you want to link or bind to persistent
storage. A file can be represented either as a string, simply

View file

@ -47,44 +47,17 @@ fi
sourceBase="${sourceBase%/}"
target="${target%/}"
# check that the source exists and warn the user if it doesn't
# check that the source exists and warn the user if it doesn't, then
# create them with the specified permissions
realSource="$(realpath -m "$sourceBase$target")"
if [[ ! -d "$realSource" ]]; then
printf "Warning: Source directory '%s' does not exist; it will be created for you with the following permissions: owner: '%s:%s', mode: '%s'.\n" "$realSource" "$user" "$group" "$mode"
mkdir --mode="$mode" "$realSource"
chown "$user:$group" "$realSource"
fi
# iterate over each part of the target path, e.g. var, lib, iwd
previousPath="/"
[[ -d "$target" ]] || mkdir "$target"
OLD_IFS=$IFS
IFS=/ # split the path on /
for pathPart in $target; do
IFS=$OLD_IFS
# skip empty parts caused by the prefix slash and multiple
# consecutive slashes
[[ "$pathPart" == "" ]] && continue
# construct the incremental path, e.g. /var, /var/lib, /var/lib/iwd
currentTargetPath="$previousPath$pathPart/"
# construct the source path, e.g. /state/var, /state/var/lib, ...
currentSourcePath="$sourceBase$currentTargetPath"
# create the source and target directories if they don't exist
if [[ ! -d "$currentSourcePath" ]]; then
mkdir --mode="$mode" "$currentSourcePath"
chown "$user:$group" "$currentSourcePath"
fi
[[ -d "$currentTargetPath" ]] || mkdir "$currentTargetPath"
# resolve the source path to avoid symlinks
currentRealSourcePath="$(realpath -m "$currentSourcePath")"
# synchronize perms between source and target
chown --reference="$currentRealSourcePath" "$currentTargetPath"
chmod --reference="$currentRealSourcePath" "$currentTargetPath"
# lastly we update the previousPath to continue down the tree
previousPath="$currentTargetPath"
done
# synchronize perms between source and target
chown --reference="$realSource" "$target"
chmod --reference="$realSource" "$target"

22
lib.nix
View file

@ -11,6 +11,9 @@ let
removePrefix
foldl'
elem
take
length
last
;
inherit (lib.strings)
sanitizeDerivationName
@ -34,6 +37,24 @@ let
in
prefix + path;
parentsOf = path:
let
prefix = optionalString (hasPrefix "/" path) "/";
split = splitPath [ path ];
parents = take ((length split) - 1) split;
in
foldl'
(state: item:
state ++ [
(concatPaths [
(if state != [ ] then last state else prefix)
item
])
])
[ ]
parents;
sanitizeName = name:
replaceStrings
[ "." ] [ "" ]
@ -63,6 +84,7 @@ in
splitPath
dirListToPath
concatPaths
parentsOf
sanitizeName
duplicates
;

105
nixos.nix
View file

@ -26,6 +26,9 @@ let
catAttrs
optional
literalExpression
optionalString
elem
mapAttrs
;
inherit (pkgs.callPackage ./lib.nix { })
@ -34,6 +37,7 @@ let
concatPaths
sanitizeName
duplicates
parentsOf
;
cfg = config.environment.persistence;
@ -147,7 +151,10 @@ in
The path to the file.
'';
};
parentDirectory = dirPermsOpts;
parentDirectory = dirPermsOpts // {
# See comment in dirOpts below for an explanation.
defaultPerms = mapAttrs (_: x: x // { internal = true; }) dirPermsOpts;
};
filePath = mkOption {
type = path;
internal = true;
@ -162,6 +169,12 @@ in
The path to the directory.
'';
};
# Save the default permissions at the level the
# directory resides. This used when creating its
# parent directories, giving them reasonable
# default permissions unaffected by the
# directory's own.
defaultPerms = mapAttrs (_: x: x // { internal = true; }) dirPermsOpts;
dirPath = mkOption {
type = path;
internal = true;
@ -172,7 +185,7 @@ in
commonOpts
fileOpts
({ config, ... }: {
parentDirectory = mkDefault defaultPerms;
parentDirectory = mkDefault (defaultPerms // { inherit defaultPerms; });
filePath = mkDefault config.file;
})
];
@ -180,6 +193,7 @@ in
commonOpts
dirOpts
({ config, ... }: {
defaultPerms = mkDefault defaultPerms;
dirPath = mkDefault config.directory;
})
] ++ (mapAttrsToList (n: v: { ${n} = mkDefault v; }) defaultPerms));
@ -200,6 +214,7 @@ in
fileConfig =
{ config, ... }:
{
parentDirectory = mkDefault (userDefaultPerms // { defaultPerms = userDefaultPerms; });
filePath =
if config.home != null then
concatPaths [ config.home config.file ]
@ -209,13 +224,13 @@ in
userFile = submodule [
commonOpts
fileOpts
{ parentDirectory = mkDefault userDefaultPerms; }
{ inherit (config) home; }
fileConfig
];
dirConfig =
{ config, ... }:
{
defaultPerms = mkDefault userDefaultPerms;
dirPath =
if config.home != null then
concatPaths [ config.home config.directory ]
@ -480,7 +495,8 @@ in
# storage directories we want to bind mount.
dirCreationScript =
let
fileDirectories = unique (map
# The parent directories of files.
fileDirs = unique (map
(f:
rec {
directory = dirOf f.file;
@ -492,11 +508,90 @@ in
inherit (f) persistentStoragePath home;
} // f.parentDirectory)
files);
# All the directories actually listed by the user and the
# parent directories of listed files.
explicitDirs = directories ++ fileDirs;
# Home directories have to be handled specially, since
# they're at the permissions boundary where they
# themselves should be owned by the user and have stricter
# permissions than regular directories, whereas its parent
# should be owned by root and have regular permissions.
#
# This simply collects all the home directories and sets
# the appropriate permissions and ownership.
homeDirs =
foldl'
(state: dir:
let
defaultPerms = {
mode = "0755";
user = "root";
group = "root";
};
homeDir = {
directory = dir.home;
dirPath = dir.home;
home = null;
mode = "0700";
user = dir.user;
group = users.${dir.user}.group;
inherit defaultPerms;
inherit (dir) persistentStoragePath;
};
in
if dir.home != null then
if !(elem homeDir state) then
state ++ [ homeDir ]
else
state
else
state)
[ ]
explicitDirs;
# Generate entries for all parent directories of the
# argument directories, listed in the order they need to
# be created. The parent directories are assigned default
# permissions.
mkParentDirs = dirs:
let
# Create a new directory item from `dir`, the child
# directory item to inherit properties from and
# `path`, the parent directory path.
mkParent = dir: path: {
directory = path;
dirPath =
if dir.home != null then
concatPaths [ dir.home path ]
else
path;
inherit (dir) persistentStoragePath home;
inherit (dir.defaultPerms) user group mode;
};
# Create new directory items for all parent
# directories of a directory.
mkParents = dir:
map (mkParent dir) (parentsOf dir.directory);
in
unique (flatten (map mkParents dirs));
# Parent directories of home folders. This is usually only
# /home, unless the user's home is in a non-standard
# location.
homeDirParents = mkParentDirs homeDirs;
# Parent directories of all explicitly listed directories.
parentDirs = mkParentDirs explicitDirs;
# All directories in the order they should be created.
allDirs = homeDirParents ++ homeDirs ++ parentDirs ++ explicitDirs;
in
pkgs.writeShellScript "impermanence-run-create-directories" ''
_status=0
trap "_status=1" ERR
${concatMapStrings mkDirWithPerms (directories ++ fileDirectories)}
${concatMapStrings mkDirWithPerms allDirs}
exit $_status
'';