mirror of
https://github.com/NixOS/nixpkgs.git
synced 2024-11-15 22:36:23 +01:00
01543e789c
genJqReplacementSnippet quotes the content of the secret file in the output json file, which prevents structured secret, such as a list or an object, from being used. This commit adds a `quote = true|false` option to the `{ _secret = "/path/to/secret"; }` attribute set. `quote = true` treats the content of /path/to/secret as string, while `quote = false` treats the content of /path/to/secret as a JSON document. `quote = true` is the default, maintaining backward compatibility.
355 lines
12 KiB
Nix
355 lines
12 KiB
Nix
{ lib, config, pkgs }:
|
|
|
|
let
|
|
inherit (lib)
|
|
any
|
|
attrNames
|
|
concatMapStringsSep
|
|
concatStringsSep
|
|
elem
|
|
escapeShellArg
|
|
filter
|
|
flatten
|
|
getName
|
|
hasPrefix
|
|
hasSuffix
|
|
imap0
|
|
imap1
|
|
isAttrs
|
|
isDerivation
|
|
isFloat
|
|
isInt
|
|
isList
|
|
isPath
|
|
isString
|
|
listToAttrs
|
|
mapAttrs
|
|
nameValuePair
|
|
optionalString
|
|
removePrefix
|
|
removeSuffix
|
|
replaceStrings
|
|
stringToCharacters
|
|
types
|
|
;
|
|
|
|
inherit (lib.strings) toJSON normalizePath escapeC;
|
|
in
|
|
|
|
let
|
|
utils = rec {
|
|
|
|
# Copy configuration files to avoid having the entire sources in the system closure
|
|
copyFile = filePath: pkgs.runCommand (builtins.unsafeDiscardStringContext (baseNameOf filePath)) {} ''
|
|
cp ${filePath} $out
|
|
'';
|
|
|
|
# Check whenever fileSystem is needed for boot. NOTE: Make sure
|
|
# pathsNeededForBoot is closed under the parent relationship, i.e. if /a/b/c
|
|
# is in the list, put /a and /a/b in as well.
|
|
pathsNeededForBoot = [ "/" "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/var/lib/nixos" "/etc" "/usr" ];
|
|
fsNeededForBoot = fs: fs.neededForBoot || elem fs.mountPoint pathsNeededForBoot;
|
|
|
|
# Check whenever `b` depends on `a` as a fileSystem
|
|
fsBefore = a: b:
|
|
let
|
|
# normalisePath adds a slash at the end of the path if it didn't already
|
|
# have one.
|
|
#
|
|
# The reason slashes are added at the end of each path is to prevent `b`
|
|
# from accidentally depending on `a` in cases like
|
|
# a = { mountPoint = "/aaa"; ... }
|
|
# b = { device = "/aaaa"; ... }
|
|
# Here a.mountPoint *is* a prefix of b.device even though a.mountPoint is
|
|
# *not* a parent of b.device. If we add a slash at the end of each string,
|
|
# though, this is not a problem: "/aaa/" is not a prefix of "/aaaa/".
|
|
normalisePath = path: "${path}${optionalString (!(hasSuffix "/" path)) "/"}";
|
|
normalise = mount: mount // { device = normalisePath (toString mount.device);
|
|
mountPoint = normalisePath mount.mountPoint;
|
|
depends = map normalisePath mount.depends;
|
|
};
|
|
|
|
a' = normalise a;
|
|
b' = normalise b;
|
|
|
|
in hasPrefix a'.mountPoint b'.device
|
|
|| hasPrefix a'.mountPoint b'.mountPoint
|
|
|| any (hasPrefix a'.mountPoint) b'.depends;
|
|
|
|
# Escape a path according to the systemd rules. FIXME: slow
|
|
# The rules are described in systemd.unit(5) as follows:
|
|
# The escaping algorithm operates as follows: given a string, any "/" character is replaced by "-", and all other characters which are not ASCII alphanumerics, ":", "_" or "." are replaced by C-style "\x2d" escapes. In addition, "." is replaced with such a C-style escape when it would appear as the first character in the escaped string.
|
|
# When the input qualifies as absolute file system path, this algorithm is extended slightly: the path to the root directory "/" is encoded as single dash "-". In addition, any leading, trailing or duplicate "/" characters are removed from the string before transformation. Example: /foo//bar/baz/ becomes "foo-bar-baz".
|
|
escapeSystemdPath = s: let
|
|
replacePrefix = p: r: s: (if (hasPrefix p s) then r + (removePrefix p s) else s);
|
|
trim = s: removeSuffix "/" (removePrefix "/" s);
|
|
normalizedPath = normalizePath s;
|
|
in
|
|
replaceStrings ["/"] ["-"]
|
|
(replacePrefix "." (escapeC ["."] ".")
|
|
(escapeC (stringToCharacters " !\"#$%&'()*+,;<=>=@[\\]^`{|}~-")
|
|
(if normalizedPath == "/" then normalizedPath else trim normalizedPath)));
|
|
|
|
# Quotes an argument for use in Exec* service lines.
|
|
# systemd accepts "-quoted strings with escape sequences, toJSON produces
|
|
# a subset of these.
|
|
# Additionally we escape % to disallow expansion of % specifiers. Any lone ;
|
|
# in the input will be turned it ";" and thus lose its special meaning.
|
|
# Every $ is escaped to $$, this makes it unnecessary to disable environment
|
|
# substitution for the directive.
|
|
escapeSystemdExecArg = arg:
|
|
let
|
|
s = if isPath arg then "${arg}"
|
|
else if isString arg then arg
|
|
else if isInt arg || isFloat arg || isDerivation arg then toString arg
|
|
else throw "escapeSystemdExecArg only allows strings, paths, numbers and derivations";
|
|
in
|
|
replaceStrings [ "%" "$" ] [ "%%" "$$" ] (toJSON s);
|
|
|
|
# Quotes a list of arguments into a single string for use in a Exec*
|
|
# line.
|
|
escapeSystemdExecArgs = concatMapStringsSep " " escapeSystemdExecArg;
|
|
|
|
# Returns a system path for a given shell package
|
|
toShellPath = shell:
|
|
if types.shellPackage.check shell then
|
|
"/run/current-system/sw${shell.shellPath}"
|
|
else if types.package.check shell then
|
|
throw "${shell} is not a shell package"
|
|
else
|
|
shell;
|
|
|
|
/* Recurse into a list or an attrset, searching for attrs named like
|
|
the value of the "attr" parameter, and return an attrset where the
|
|
names are the corresponding jq path where the attrs were found and
|
|
the values are the values of the attrs.
|
|
|
|
Example:
|
|
recursiveGetAttrWithJqPrefix {
|
|
example = [
|
|
{
|
|
irrelevant = "not interesting";
|
|
}
|
|
{
|
|
ignored = "ignored attr";
|
|
relevant = {
|
|
secret = {
|
|
_secret = "/path/to/secret";
|
|
};
|
|
};
|
|
}
|
|
];
|
|
} "_secret" -> { ".example[1].relevant.secret" = "/path/to/secret"; }
|
|
*/
|
|
recursiveGetAttrWithJqPrefix = item: attr: mapAttrs (_name: set: set.${attr}) (recursiveGetAttrsetWithJqPrefix item attr);
|
|
|
|
/* Similar to `recursiveGetAttrWithJqPrefix`, but returns the whole
|
|
attribute set containing `attr` instead of the value of `attr` in
|
|
the set.
|
|
|
|
Example:
|
|
recursiveGetAttrsetWithJqPrefix {
|
|
example = [
|
|
{
|
|
irrelevant = "not interesting";
|
|
}
|
|
{
|
|
ignored = "ignored attr";
|
|
relevant = {
|
|
secret = {
|
|
_secret = "/path/to/secret";
|
|
quote = true;
|
|
};
|
|
};
|
|
}
|
|
];
|
|
} "_secret" -> { ".example[1].relevant.secret" = { _secret = "/path/to/secret"; quote = true; }; }
|
|
*/
|
|
recursiveGetAttrsetWithJqPrefix = item: attr:
|
|
let
|
|
recurse = prefix: item:
|
|
if item ? ${attr} then
|
|
nameValuePair prefix item
|
|
else if isDerivation item then []
|
|
else if isAttrs item then
|
|
map (name:
|
|
let
|
|
escapedName = ''"${replaceStrings [''"'' "\\"] [''\"'' "\\\\"] name}"'';
|
|
in
|
|
recurse (prefix + "." + escapedName) item.${name}) (attrNames item)
|
|
else if isList item then
|
|
imap0 (index: item: recurse (prefix + "[${toString index}]") item) item
|
|
else
|
|
[];
|
|
in listToAttrs (flatten (recurse "" item));
|
|
|
|
/* Takes an attrset and a file path and generates a bash snippet that
|
|
outputs a JSON file at the file path with all instances of
|
|
|
|
{ _secret = "/path/to/secret" }
|
|
|
|
in the attrset replaced with the contents of the file
|
|
"/path/to/secret" in the output JSON.
|
|
|
|
When a configuration option accepts an attrset that is finally
|
|
converted to JSON, this makes it possible to let the user define
|
|
arbitrary secret values.
|
|
|
|
Example:
|
|
If the file "/path/to/secret" contains the string
|
|
"topsecretpassword1234",
|
|
|
|
genJqSecretsReplacementSnippet {
|
|
example = [
|
|
{
|
|
irrelevant = "not interesting";
|
|
}
|
|
{
|
|
ignored = "ignored attr";
|
|
relevant = {
|
|
secret = {
|
|
_secret = "/path/to/secret";
|
|
};
|
|
};
|
|
}
|
|
];
|
|
} "/path/to/output.json"
|
|
|
|
would generate a snippet that, when run, outputs the following
|
|
JSON file at "/path/to/output.json":
|
|
|
|
{
|
|
"example": [
|
|
{
|
|
"irrelevant": "not interesting"
|
|
},
|
|
{
|
|
"ignored": "ignored attr",
|
|
"relevant": {
|
|
"secret": "topsecretpassword1234"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
|
|
The attribute set { _secret = "/path/to/secret"; } can contain extra
|
|
options, currently it accepts the `quote = true|false` option.
|
|
|
|
If `quote = true` (default behavior), the content of the secret file will
|
|
be quoted as a string and embedded. Otherwise, if `quote = false`, the
|
|
content of the secret file will be parsed to JSON and then embedded.
|
|
|
|
Example:
|
|
If the file "/path/to/secret" contains the JSON document:
|
|
|
|
[
|
|
{ "a": "topsecretpassword1234" },
|
|
{ "b": "topsecretpassword5678" }
|
|
]
|
|
|
|
genJqSecretsReplacementSnippet {
|
|
example = [
|
|
{
|
|
irrelevant = "not interesting";
|
|
}
|
|
{
|
|
ignored = "ignored attr";
|
|
relevant = {
|
|
secret = {
|
|
_secret = "/path/to/secret";
|
|
quote = false;
|
|
};
|
|
};
|
|
}
|
|
];
|
|
} "/path/to/output.json"
|
|
|
|
would generate a snippet that, when run, outputs the following
|
|
JSON file at "/path/to/output.json":
|
|
|
|
{
|
|
"example": [
|
|
{
|
|
"irrelevant": "not interesting"
|
|
},
|
|
{
|
|
"ignored": "ignored attr",
|
|
"relevant": {
|
|
"secret": [
|
|
{ "a": "topsecretpassword1234" },
|
|
{ "b": "topsecretpassword5678" }
|
|
]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
*/
|
|
genJqSecretsReplacementSnippet = genJqSecretsReplacementSnippet' "_secret";
|
|
|
|
# Like genJqSecretsReplacementSnippet, but allows the name of the
|
|
# attr which identifies the secret to be changed.
|
|
genJqSecretsReplacementSnippet' = attr: set: output:
|
|
let
|
|
secretsRaw = recursiveGetAttrsetWithJqPrefix set attr;
|
|
# Set default option values
|
|
secrets = mapAttrs (_name: set: {
|
|
quote = true;
|
|
} // set) secretsRaw;
|
|
stringOrDefault = str: def: if str == "" then def else str;
|
|
in ''
|
|
if [[ -h '${output}' ]]; then
|
|
rm '${output}'
|
|
fi
|
|
|
|
inherit_errexit_enabled=0
|
|
shopt -pq inherit_errexit && inherit_errexit_enabled=1
|
|
shopt -s inherit_errexit
|
|
''
|
|
+ concatStringsSep
|
|
"\n"
|
|
(imap1 (index: name: ''
|
|
secret${toString index}=$(<'${secrets.${name}.${attr}}')
|
|
export secret${toString index}
|
|
'')
|
|
(attrNames secrets))
|
|
+ "\n"
|
|
+ "${pkgs.jq}/bin/jq >'${output}' "
|
|
+ escapeShellArg (stringOrDefault
|
|
(concatStringsSep
|
|
" | "
|
|
(imap1 (index: name: ''${name} = ($ENV.secret${toString index}${optionalString (!secrets.${name}.quote) " | fromjson"})'')
|
|
(attrNames secrets)))
|
|
".")
|
|
+ ''
|
|
<<'EOF'
|
|
${toJSON set}
|
|
EOF
|
|
(( ! $inherit_errexit_enabled )) && shopt -u inherit_errexit
|
|
'';
|
|
|
|
/* Remove packages of packagesToRemove from packages, based on their names.
|
|
Relies on package names and has quadratic complexity so use with caution!
|
|
|
|
Type:
|
|
removePackagesByName :: [package] -> [package] -> [package]
|
|
|
|
Example:
|
|
removePackagesByName [ nautilus file-roller ] [ file-roller totem ]
|
|
=> [ nautilus ]
|
|
*/
|
|
removePackagesByName = packages: packagesToRemove:
|
|
let
|
|
namesToRemove = map getName packagesToRemove;
|
|
in
|
|
filter (x: !(elem (getName x) namesToRemove)) packages;
|
|
|
|
systemdUtils = {
|
|
lib = import ./systemd-lib.nix { inherit lib config pkgs utils; };
|
|
unitOptions = import ./systemd-unit-options.nix { inherit lib systemdUtils; };
|
|
types = import ./systemd-types.nix { inherit lib systemdUtils pkgs; };
|
|
network = {
|
|
units = import ./systemd-network-units.nix { inherit lib systemdUtils; };
|
|
};
|
|
};
|
|
};
|
|
in utils
|