Merge branch 'master' into pkgs/blake3-1.7.0

This commit is contained in:
Jörg Thalheim
2025-03-24 10:39:55 +01:00
committed by GitHub
1800 changed files with 32479 additions and 23676 deletions
@@ -20,7 +20,7 @@ jobs:
lib
maintainers
- uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
with:
extra_nix_config: sandbox = true
+1 -1
View File
@@ -44,7 +44,7 @@ jobs:
rev=$(jq -r .rev ci/pinned-nixpkgs.json)
echo "url=https://github.com/NixOS/nixpkgs/archive/$rev.tar.gz" >> "$GITHUB_ENV"
- uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
with:
extra_nix_config: sandbox = true
nix_path: nixpkgs=${{ env.url }}
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
rev=$(jq -r .rev ci/pinned-nixpkgs.json)
echo "url=https://github.com/NixOS/nixpkgs/archive/$rev.tar.gz" >> "$GITHUB_ENV"
- uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
with:
extra_nix_config: sandbox = true
nix_path: nixpkgs=${{ env.url }}
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
with:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
- uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
- name: Build shell
run: nix-build shell.nix
+2 -2
View File
@@ -45,7 +45,7 @@ jobs:
needs: get-merge-commit
if: github.repository_owner == 'NixOS' && needs.get-merge-commit.outputs.mergedSha
steps:
- uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
@@ -90,7 +90,7 @@ jobs:
runs-on: ubuntu-24.04
if: github.repository_owner == 'NixOS'
steps:
- uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
# Important: Because we use pull_request_target, this checks out the base branch of the PR, not the PR head.
# This is intentional, because we need to request the review of owners as declared in the base branch.
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
with:
ref: ${{ needs.get-merge-commit.outputs.mergedSha }}
- uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
with:
# nixpkgs commit is pinned so that it doesn't break
# editorconfig-checker 2.4.0
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
with:
ref: ${{ needs.get-merge-commit.outputs.mergedSha }}
- uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
with:
extra_nix_config: sandbox = true
+5 -5
View File
@@ -43,7 +43,7 @@ jobs:
echo "targetSha=$targetSha" >> "$GITHUB_OUTPUT"
- name: Install Nix
uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
with:
extra_nix_config: sandbox = true
@@ -71,7 +71,7 @@ jobs:
path: nixpkgs
- name: Install Nix
uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
with:
extra_nix_config: sandbox = true
@@ -108,7 +108,7 @@ jobs:
path: nixpkgs
- name: Install Nix
uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
with:
extra_nix_config: sandbox = true
@@ -149,7 +149,7 @@ jobs:
path: nixpkgs
- name: Install Nix
uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
with:
extra_nix_config: sandbox = true
@@ -254,7 +254,7 @@ jobs:
path: comparison
- name: Install Nix
uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
# Important: This workflow job runs with extra permissions,
# so we need to make sure to not run untrusted code from PRs
+21 -5
View File
@@ -19,13 +19,21 @@ permissions: {}
jobs:
nixos:
name: nixos-manual-build
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
system:
- x86_64-linux
- aarch64-linux
runs-on: >-
${{ (matrix.system == 'x86_64-linux' && 'ubuntu-24.04')
|| (matrix.system == 'aarch64-linux' && 'ubuntu-24.04-arm') }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
- uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
with:
extra_nix_config: sandbox = true
@@ -34,7 +42,15 @@ jobs:
with:
# This cache is for the nixpkgs repo checks and should not be trusted or used elsewhere.
name: nixpkgs-ci
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Building NixOS manual
run: NIX_PATH=nixpkgs=$(pwd) nix-build --option restrict-eval true nixos/release.nix -A manual.x86_64-linux
- name: Build NixOS manual
id: build-manual
run: NIX_PATH=nixpkgs=$(pwd) nix-build --option restrict-eval true nixos/release.nix -A manual.${{ matrix.system }}
- name: Upload NixOS manual
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: nixos-manual-${{ matrix.system }}
path: result/
if-no-files-found: error
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
with:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
- uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
with:
extra_nix_config: sandbox = true
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
ref: ${{ needs.get-merge-commit.outputs.mergedSha }}
if: ${{ env.CHANGED_FILES && env.CHANGED_FILES != '' }}
- uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
with:
extra_nix_config: sandbox = true
nix_path: nixpkgs=channel:nixpkgs-unstable
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
git worktree add "$target" "$(git rev-parse HEAD^1)"
echo "target=$target" >> "$GITHUB_ENV"
- uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
- name: Fetching the pinned tool
# Update the pinned version using ci/nixpkgs-vet/update-pinned-tool.sh
+9 -1
View File
@@ -149,10 +149,13 @@ nixos/modules/installer/tools/nix-fallback-paths.nix @NixOS/nix-team @raitobeza
/nixos/modules/services/monitoring/amazon-cloudwatch-agent.nix @philipmw
/nixos/tests/amazon-cloudwatch-agent.nix @philipmw
# Monitoring
/nixos/modules/services/monitoring/fluent-bit.nix @arianvp
/nixos/tests/fluent-bit.nix @arianvp
# nixos-rebuild-ng
/pkgs/by-name/ni/nixos-rebuild-ng @thiagokokada
# Updaters
## update.nix
/maintainers/scripts/update.nix @jtojnar
@@ -399,6 +402,11 @@ pkgs/by-name/fo/forgejo/ @adamcstephens @bendlas @emilylange
/pkgs/development/compilers/ocaml @ulrikstrid
/pkgs/development/ocaml-modules @ulrikstrid
# ZFS
/nixos/modules/tasks/filesystems/zfs.nix @adamcstephens @amarshall
/nixos/tests/zfs.nix @adamcstephens @amarshall
/pkgs/os-specific/linux/zfs @adamcstephens @amarshall
# Zig
/pkgs/development/compilers/zig @figsoda @RossComputerGuy
/doc/hooks/zig.section.md @figsoda @RossComputerGuy
+9 -1
View File
@@ -71,7 +71,15 @@ let
getLabels
;
getAttrs = dir: builtins.fromJSON (builtins.readFile "${dir}/outpaths.json");
getAttrs =
dir:
let
raw = builtins.readFile "${dir}/outpaths.json";
# The file contains Nix paths; we need to ignore them for evaluation purposes,
# else there will be a "is not allowed to refer to a store path" error.
data = builtins.unsafeDiscardStringContext raw;
in
builtins.fromJSON data;
beforeAttrs = getAttrs beforeResultDir;
afterAttrs = getAttrs afterResultDir;
+1
View File
@@ -108,6 +108,7 @@ A few markups for other kinds of literals are also available:
These literal kinds are used mostly in NixOS option documentation.
This syntax is taken from [MyST](https://myst-parser.readthedocs.io/en/latest/syntax/syntax.html#roles-an-in-line-extension-point). Though, the feature originates from [reStructuredText](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-manpage) with slightly different syntax.
They are handled by `myst_role` defined per renderer. <!-- reverse references in code -->
#### Admonitions
+12 -7
View File
@@ -99,12 +99,17 @@ failure. To prevent this, guard the completion generation commands.
```nix
{
nativeBuildInputs = [ installShellFiles ];
postInstall = lib.optionalString (stdenv.buildPlatform.canExecute stdenv.hostPlatform) ''
# using named fd
installShellCompletion --cmd foobar \
--bash <($out/bin/foobar --bash-completion) \
--fish <($out/bin/foobar --fish-completion) \
--zsh <($out/bin/foobar --zsh-completion)
'';
postInstall = lib.optionalString (stdenv.hostPlatform.emulatorAvailable buildPackages) (
let
emulator = stdenv.hostPlatform.emulator buildPackages;
in
''
# using named fd
installShellCompletion --cmd foobar \
--bash <(${emulator} $out/bin/foobar --bash-completion) \
--fish <(${emulator} $out/bin/foobar --fish-completion) \
--zsh <(${emulator} $out/bin/foobar --zsh-completion)
''
);
}
```
+1
View File
@@ -14,6 +14,7 @@ fish.section.md
fuse.section.md
geant4.section.md
ibus.section.md
inkscape.section.md
kakoune.section.md
krita.section.md
linux.section.md
+29
View File
@@ -0,0 +1,29 @@
# Inkscape {#sec-inkscape}
[Inkscape](https://inkscape.org) is a powerful vector graphics editor.
## Plugins {#inkscape-plugins}
Inkscape plugins are collected in the [`inkscape-extensions`](https://search.nixos.org/packages?channel=unstable&type=packages&query=cudaPackages) package set.
To enable them, use an override on `inkscape-with-extensions`:
```nix
inkscape-with-extensions.override {
inkscapeExtensions = with inkscape-extensions; [
inkstitch
];
}
```
Similarly, this works in the shell:
```bash
$ nix-shell -p 'inkscape-with-extensions.override { inkscapeExtensions = with inkscape-extensions; [inkstitch]; }'
[nix-shell:~]$ # Ink/Stitch is now available via the extension menu
[nix-shell:~]$ inkscape
```
All available extension can be enabled by passing `inkscapeExtensions = null;`.
::: {.note}
Loading the Inkscape extensions stand-alone (without using `override`) does not affect Inkscape at all.
:::
+6
View File
@@ -17,6 +17,9 @@
"ex-testEqualArrayOrMap-test-function-add-cowbell": [
"index.html#ex-testEqualArrayOrMap-test-function-add-cowbell"
],
"inkscape-plugins": [
"index.html#inkscape-plugins"
],
"neovim": [
"index.html#neovim"
],
@@ -62,6 +65,9 @@
"sec-build-helper-extendMkDerivation": [
"index.html#sec-build-helper-extendMkDerivation"
],
"sec-inkscape": [
"index.html#sec-inkscape"
],
"sec-language-cosmic": [
"index.html#sec-language-cosmic"
],
+5
View File
@@ -75,6 +75,11 @@
- `functor.wrapped` is now deprecated for some types and using it will give a warning with migration instructions. It is deprecated for these types:
- `lib.types.attrsWith`
- `lib.types.listOf`
- `lib.types.unique` and `lib.types.uniq`
- `lib.types.nullOr`
- `lib.types.functionTo`
- `lib.types.coercedTo`
- `lib.types.either`
- Plasma 5 and Qt 5 based versions of associated software are deprecated in NixOS 25.05, and will be removed in NixOS 25.11. Users are encouraged to upgrade to Plasma 6.
+1
View File
@@ -156,6 +156,7 @@ let
makeScope makeScopeWithSplicing makeScopeWithSplicing'
extendMkDerivation;
inherit (self.derivations) lazyDerivation optionalDrvAttr warnOnInstantiate;
inherit (self.generators) mkLuaInline;
inherit (self.meta) addMetaAttrs dontDistribute setName updateName
appendToName mapDerivationAttrset setPrio lowPrio lowPrioSet hiPrio
hiPrioSet licensesSpdx getLicenseFromSpdxId getLicenseFromSpdxIdOr
+2 -2
View File
@@ -743,6 +743,8 @@ in rec {
"nil"
else if isInt v || isFloat v || isString v || isBool v then
toJSON v
else if isPath v || isDerivation v then
toJSON "${v}"
else if isList v then
(if v == [ ] then "{}" else
"{${introSpace}${concatItems (map (value: "${toLua innerArgs value}") v)}${outroSpace}}")
@@ -752,8 +754,6 @@ in rec {
"(${v.expr})"
else if v == { } then
"{}"
else if isDerivation v then
''"${toString v}"''
else
"{${introSpace}${concatItems (
mapAttrsToList (key: value: "[${toJSON key}] = ${toLua innerArgs value}") v
+5 -5
View File
@@ -140,11 +140,6 @@ lib.mapAttrs mkLicense ({
fullName = "Apache License 2.0";
};
asl20-llvm = {
spdxId = "Apache-2.0 WITH LLVM-exception";
fullName = "Apache License 2.0 with LLVM Exceptions";
};
bitstreamVera = {
spdxId = "Bitstream-Vera";
fullName = "Bitstream Vera Font License";
@@ -859,6 +854,11 @@ lib.mapAttrs mkLicense ({
url = "https://opensource.franz.com/preamble.html";
};
llvm-exception = {
spdxId = "LLVM-exception";
fullName = "LLVM Exception"; # LLVM exceptions to the Apache 2.0 License
};
lppl1 = {
spdxId = "LPPL-1.0";
fullName = "LaTeX Project Public License v1.0";
+32
View File
@@ -415,6 +415,38 @@ NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribu
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.listOf.type.nestedTypes.elemType.functor.wrapped ./deprecated-wrapped.nix
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.mergedListOf.type.nestedTypes.elemType.functor.wrapped ./deprecated-wrapped.nix
# unique / uniq
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.unique.type.functor.wrapped ./deprecated-wrapped.nix
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.mergedUnique.type.functor.wrapped ./deprecated-wrapped.nix
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.unique.type.nestedTypes.elemType.functor.wrapped ./deprecated-wrapped.nix
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.mergedUnique.type.nestedTypes.elemType.functor.wrapped ./deprecated-wrapped.nix
# nullOr
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.nullOr.type.functor.wrapped ./deprecated-wrapped.nix
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.mergedNullOr.type.functor.wrapped ./deprecated-wrapped.nix
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.nullOr.type.nestedTypes.elemType.functor.wrapped ./deprecated-wrapped.nix
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.mergedNullOr.type.nestedTypes.elemType.functor.wrapped ./deprecated-wrapped.nix
# functionTo
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.functionTo.type.functor.wrapped ./deprecated-wrapped.nix
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.mergedFunctionTo.type.functor.wrapped ./deprecated-wrapped.nix
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.functionTo.type.nestedTypes.elemType.functor.wrapped ./deprecated-wrapped.nix
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.mergedFunctionTo.type.nestedTypes.elemType.functor.wrapped ./deprecated-wrapped.nix
# coercedTo
# Note: test 'nestedTypes.finalType' and 'nestedTypes.coercedType'
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.coercedTo.type.functor.wrapped ./deprecated-wrapped.nix
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.coercedTo.type.nestedTypes.finalType.functor.wrapped ./deprecated-wrapped.nix
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.coercedTo.type.nestedTypes.coercedType.functor.wrapped ./deprecated-wrapped.nix
# either
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.either.type.functor.wrapped ./deprecated-wrapped.nix
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.mergedEither.type.functor.wrapped ./deprecated-wrapped.nix
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.either.type.nestedTypes.left.functor.wrapped ./deprecated-wrapped.nix
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.either.type.nestedTypes.right.functor.wrapped ./deprecated-wrapped.nix
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.mergedEither.type.nestedTypes.left.functor.wrapped ./deprecated-wrapped.nix
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.mergedEither.type.nestedTypes.right.functor.wrapped ./deprecated-wrapped.nix
# Even with multiple assignments, a type error should be thrown if any of them aren't valid
checkConfigError 'A definition for option .* is not of type .*' \
+49
View File
@@ -6,6 +6,11 @@ let
# attrsOf uses attrsWith internally
attrsOf
listOf
unique
nullOr
functionTo
coercedTo
either
;
in
{
@@ -26,6 +31,38 @@ in
options.mergedListOf = mkOption {
type = listOf (listOf types.str);
};
# unique
options.unique = mkOption {
type = unique { message = ""; } (listOf types.str);
};
options.mergedUnique = mkOption {
type = unique { message = ""; } (listOf types.str);
};
# nullOr
options.nullOr = mkOption {
type = nullOr (listOf types.str);
};
options.mergedNullOr = mkOption {
type = nullOr (listOf types.str);
};
# functionTo
options.functionTo = mkOption {
type = functionTo (listOf types.str);
};
options.mergedFunctionTo = mkOption {
type = functionTo (listOf types.str);
};
# coercedTo
# Note: coercedTo is a non-mergeable option-type
options.coercedTo = mkOption {
type = coercedTo (listOf types.str) lib.id (listOf types.str);
};
options.either = mkOption {
type = either (listOf types.str) (listOf types.str);
};
options.mergedEither = mkOption {
type = either (listOf types.str) (listOf types.str);
};
}
)
# Module B
@@ -38,6 +75,18 @@ in
options.mergedListOf = mkOption {
type = listOf (listOf types.str);
};
options.mergedUnique = mkOption {
type = unique { message = ""; } (listOf types.str);
};
options.mergedNullOr = mkOption {
type = nullOr (listOf types.str);
};
options.mergedFunctionTo = mkOption {
type = functionTo (listOf types.str);
};
options.mergedEither = mkOption {
type = either (listOf types.str) (listOf types.str);
};
}
)
];
+28 -10
View File
@@ -75,6 +75,7 @@ let
# Note that individual attributes can be overriden if needed.
elemTypeFunctor = name: { elemType, ... }@payload: {
inherit name payload;
wrappedDeprecationMessage = makeWrappedDeprecationMessage payload;
type = outer_types.types.${name};
binOp = a: b:
let
@@ -85,10 +86,10 @@ let
null
else
{ elemType = merged; };
wrappedDeprecationMessage = { loc }: lib.warn ''
The deprecated `${lib.optionalString (loc != null) "type."}functor.wrapped` attribute ${lib.optionalString (loc != null) "of the option `${showOption loc}` "}is accessed, use `${lib.optionalString (loc != null) "type."}nestedTypes.elemType` instead.
'' payload.elemType;
};
makeWrappedDeprecationMessage = payload: { loc }: lib.warn ''
The deprecated `${lib.optionalString (loc != null) "type."}functor.wrapped` attribute ${lib.optionalString (loc != null) "of the option `${showOption loc}` "}is accessed, use `${lib.optionalString (loc != null) "type."}nestedTypes.elemType` instead.
'' payload.elemType;
outer_types =
@@ -834,6 +835,15 @@ rec {
};
};
# A value produced by `lib.mkLuaInline`
luaInline = mkOptionType {
name = "luaInline";
description = "inline lua";
descriptionClass = "noun";
check = x: x._type or null == "lua-inline";
merge = mergeEqualOption;
};
uniq = unique { message = ""; };
unique = { message }: type: mkOptionType rec {
@@ -844,7 +854,9 @@ rec {
getSubOptions = type.getSubOptions;
getSubModules = type.getSubModules;
substSubModules = m: uniq (type.substSubModules m);
functor = (defaultFunctor name) // { wrapped = type; };
functor = elemTypeFunctor name { elemType = type; } // {
type = payload: types.unique { inherit message; } payload.elemType;
};
nestedTypes.elemType = type;
};
@@ -864,7 +876,9 @@ rec {
getSubOptions = elemType.getSubOptions;
getSubModules = elemType.getSubModules;
substSubModules = m: nullOr (elemType.substSubModules m);
functor = (defaultFunctor name) // { wrapped = elemType; };
functor = (elemTypeFunctor name { inherit elemType; }) // {
type = payload: types.nullOr payload.elemType;
};
nestedTypes.elemType = elemType;
};
@@ -883,7 +897,9 @@ rec {
getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "<function body>" ]);
getSubModules = elemType.getSubModules;
substSubModules = m: functionTo (elemType.substSubModules m);
functor = (defaultFunctor "functionTo") // { wrapped = elemType; };
functor = (elemTypeFunctor "functionTo" { inherit elemType; }) // {
type = payload: types.functionTo payload.elemType;
};
nestedTypes.elemType = elemType;
};
@@ -1118,13 +1134,13 @@ rec {
then t2.merge loc defs
else mergeOneOption loc defs;
typeMerge = f':
let mt1 = t1.typeMerge (elemAt f'.wrapped 0).functor;
mt2 = t2.typeMerge (elemAt f'.wrapped 1).functor;
let mt1 = t1.typeMerge (elemAt f'.payload.elemType 0).functor;
mt2 = t2.typeMerge (elemAt f'.payload.elemType 1).functor;
in
if (name == f'.name) && (mt1 != null) && (mt2 != null)
then functor.type mt1 mt2
else null;
functor = (defaultFunctor name) // { wrapped = [ t1 t2 ]; };
functor = elemTypeFunctor name { elemType = [ t1 t2 ]; };
nestedTypes.left = t1;
nestedTypes.right = t2;
};
@@ -1157,7 +1173,9 @@ rec {
getSubModules = finalType.getSubModules;
substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m);
typeMerge = t: null;
functor = (defaultFunctor name) // { wrapped = finalType; };
functor = (defaultFunctor name) // {
wrappedDeprecationMessage = makeWrappedDeprecationMessage { elemType = finalType; };
};
nestedTypes.coercedType = coercedType;
nestedTypes.finalType = finalType;
};
+122 -1
View File
@@ -579,6 +579,17 @@
githubId = 50264672;
name = "Adam Freeth";
};
adamperkowski = {
name = "Adam Perkowski";
email = "adas1per@protonmail.com";
matrix = "@xx0a_q:matrix.org";
github = "adamperkowski";
githubId = 75480869;
keys = [
{ fingerprint = "00F6 1623 FB56 BC5B B709 4E63 4CE6 C117 2DF6 BE79"; }
{ fingerprint = "5A53 0832 DA91 20B0 CA57 DDB6 7CBD B58E CF1D 3478"; }
];
};
adamt = {
email = "mail@adamtulinius.dk";
github = "adamtulinius";
@@ -1081,6 +1092,12 @@
githubId = 45104896;
name = "Alexandru Nechita";
};
alexandrutocar = {
email = "at@myquiet.place";
github = "alexandrutocar";
githubId = 65486851;
name = "Alexandru Tocar";
};
alexarice = {
email = "alexrice999@hotmail.co.uk";
github = "alexarice";
@@ -1185,6 +1202,18 @@
githubId = 30437811;
name = "Alex Andrews";
};
alikindsys = {
email = "alice@blocovermelho.org";
github = "alikindsys";
githubId = 36565196;
name = "Alikind System";
keys = [
{
fingerprint = "7D31 15DC D912 C15A 2781 F7BB 511C B44B C752 2A89";
}
];
};
alirezameskin = {
email = "alireza.meskin@gmail.com";
github = "alirezameskin";
@@ -6387,7 +6416,7 @@
name = "Sebastian Krohn";
};
drawbu = {
email = "clement2104.boillot@gmail.com";
email = "nixpkgs@drawbu.dev";
github = "drawbu";
githubId = 69208565;
name = "Clément Boillot";
@@ -9103,6 +9132,12 @@
githubId = 39066502;
name = "Guekka";
};
guelakais = {
email = "koroyeldiores@gmail.com";
github = "Guelakais";
githubId = 76840985;
name = "GueLaKais";
};
guibert = {
email = "david.guibert@gmail.com";
github = "dguibert";
@@ -9176,6 +9211,11 @@
github = "gytis-ivaskevicius";
githubId = 23264966;
};
GZGavinZhao = {
name = "Gavin Zhao";
github = "GZGavinZhao";
githubId = 74938940;
};
h3cth0r = {
name = "Hector Miranda";
email = "hector.miranda@tec.mx";
@@ -10514,6 +10554,12 @@
githubId = 94313;
name = "Xianyi Lin";
};
izelnakri = {
email = "contact@izelnakri.com";
github = "izelnakri";
githubId = 1190931;
name = "Izel Nakri";
};
izorkin = {
email = "Izorkin@gmail.com";
github = "Izorkin";
@@ -11415,6 +11461,13 @@
githubId = 22916782;
name = "Joan Massachs";
};
joaomoreira = {
matrix = "@joaomoreira:matrix.org";
github = "joaoymoreira";
githubId = 151087767;
name = "João Moreira";
keys = [ { fingerprint = "F457 0A3A 5F89 22F8 F572 E075 EF8B F2C8 C5F4 097D"; } ];
};
joaquintrinanes = {
email = "hi@joaquint.io";
github = "JoaquinTrinanes";
@@ -13647,6 +13700,13 @@
githubId = 10554636;
name = "Braian A. Diez";
};
linuxwhata = {
email = "linuxwhata@qq.com";
matrix = "@lwa:envs.net";
github = "linuxwhata";
githubId = 68576488;
name = "Zhou Ke";
};
lionello = {
email = "lio@lunesu.com";
github = "lionello";
@@ -14202,6 +14262,17 @@
githubId = 7910815;
name = "Alex McGrath";
};
lykos153 = {
email = "silvio.ankermann@cloudandheat.com";
github = "Lykos153";
githubId = 6453662;
name = "Silvio Ankermann";
keys = [
{
fingerprint = "8D47 6294 7205 541C 62A4 9C88 F422 6CA3 971C 4E97";
}
];
};
lyn = {
name = "Lyn";
matrix = "@lynatic:catgirl.cloud";
@@ -15098,6 +15169,13 @@
githubId = 158568;
name = "Matthias C. M. Troffaes";
};
mcparland = {
email = "john@mcpar.land";
github = "mcpar-land";
githubId = 55669980;
name = "John McParland";
keys = [ { fingerprint = "39D2 171D D733 C718 DD21 285E B326 E14B 05D8 7A4E"; } ];
};
McSinyx = {
email = "cnx@loang.net";
github = "McSinyx";
@@ -18610,6 +18688,12 @@
githubId = 88469;
name = "Jaime Breva";
};
phodina = {
email = "phodina@protonmail.com";
github = "phodina";
githubId = 2997905;
name = "Petr Hodina";
};
photex = {
email = "photex@gmail.com";
github = "photex";
@@ -20651,6 +20735,13 @@
keys = [ { fingerprint = "7DCA 5615 8AB2 621F 2F32 9FF4 1C7C E491 479F A273"; } ];
name = "Rahul Butani";
};
rseichter = {
email = "nixos.org@seichter.de";
github = "rseichter";
githubId = 30873939;
keys = [ { fingerprint = "6AE2 A847 23D5 6D98 5B34 0BC0 8E5F A470 9F69 E911"; } ];
name = "Ralph Seichter";
};
rski = {
name = "rski";
email = "rom.skiad+nix@gmail.com";
@@ -20927,6 +21018,13 @@
githubId = 7309170;
name = "Ryota Kameoka";
};
rypervenche = {
email = "git@ryper.org";
github = "rypervenche";
githubId = 1411504;
name = "rypervenche";
keys = [ { fingerprint = "1198 7A9F 03AE 47F0 4919 E334 6A41 2C4A ECE1 66EF"; } ];
};
rytone = {
email = "max@ryt.one";
github = "rastertail";
@@ -21399,6 +21497,11 @@
githubId = 19472270;
name = "Sebastian";
};
sebaguardian = {
name = "Sebaguardian";
github = "Sebaguardian";
githubId = 68247013;
};
sebastianblunt = {
name = "Sebastian Blunt";
email = "nix@sebastianblunt.com";
@@ -21532,6 +21635,12 @@
githubId = 529649;
name = "Raffael Mancini";
};
sepiabrown = {
email = "bboxone@gmail.com";
github = "sepiabrown";
githubId = 35622998;
name = "Suwon Park";
};
seppeljordan = {
email = "sebastian.jordan.mail@googlemail.com";
github = "seppeljordan";
@@ -21986,6 +22095,11 @@
githubId = 91412114;
keys = [ { fingerprint = "C1DA A551 B422 7A6F 3FD9 6B3A 467B 7D12 9EA7 3AC9"; } ];
};
silvanshade = {
github = "silvanshade";
githubId = 11022302;
name = "silvanshade";
};
Silver-Golden = {
name = "Brendan Golden";
email = "github+nixpkgs@brendan.ie";
@@ -22510,6 +22624,13 @@
githubId = 47164123;
name = "Spoonbaker";
};
sportshead = {
email = "me@sportshead.dev";
github = "sportshead";
githubId = 32637656;
name = "sportshead";
keys = [ { fingerprint = "A6B6 D031 782E BDF7 631A 8E7E A874 DB2C BFD3 CFD0"; } ];
};
sprock = {
email = "rmason@mun.ca";
github = "sprock";
+2 -1
View File
@@ -1018,8 +1018,9 @@ with lib.maintainers;
rocm = {
members = [
Madouura
Flakebi
GZGavinZhao
LunNova
mschwaig
];
githubTeams = [ "rocm-maintainers" ];
@@ -232,6 +232,13 @@ merging is handled.
definitions cannot be merged. The regular expression is processed
using `builtins.match`.
### Specialised types {#sec-option-types-specialised}
`types.luaInline`
: A string wrapped using `lib.mkLuaInline`. Allows embedding lua expressions
inline within generated lua. Multiple definitions cannot be merged.
## Submodule types {#sec-option-types-submodule}
Submodules are detailed in [Submodule](#section-option-types-submodule).
@@ -402,6 +402,31 @@ have a predefined type and string generator already declared under
: Outputs the given attribute set as an Elixir map, instead of the
default Elixir keyword list
`pkgs.formats.lua { asBindings ? false, multiline ? true, columnWidth ? 100, indentWidth ? 2, indentUsingTabs ? false }`
: A function taking an attribute set with values
`asBindings` (default `false`)
: Whether to treat attributes as variable bindings
`multiline` (default `true`)
: Whether to procude a multiline output. The output may still wrap across
multiple lines if it would otherwise exceed `columnWidth`.
`columnWidth` (default `100`)
: The column width to use to attempt to wrap lines.
`indentWidth` (default `2`)
: The width of a single indentation level.
`indentUsingTabs` (default `false`)
: Whether the indentation should use tabs instead of spaces.
`pkgs.formats.php { finalVariable }` []{#pkgs-formats-php}
: A function taking an attribute set with values
+3
View File
@@ -1592,6 +1592,9 @@
"sec-option-types-string": [
"index.html#sec-option-types-string"
],
"sec-option-types-specialised": [
"index.html#sec-option-types-specialised"
],
"sec-option-types-submodule": [
"index.html#sec-option-types-submodule"
],
@@ -38,7 +38,7 @@
- The `nixos-generate-config` command now supports a optional `--flake` option, which will generate a flake.nix file alongside the `configuration.nix` and `hardware-configuration.nix`, providing an easy instroduction into flake-based system configurations.
- A `nixos-rebuild build-image` sub-command has been added.
It allows users to build platform-specific (disk) images from their NixOS configurations. `nixos-rebuild build-image` works similar to the popular [nix-community/nixos-generators](https://github.com/nix-community/nixos-generators) project. See new [section on image building in the nixpkgs manual](https://nixos.org/manual/nixpkgs/unstable/#sec-image-nixos-rebuild-build-image). It is also available for `nixos-rebuild-ng`.
It allows users to build platform-specific (disk) images from their NixOS configurations. `nixos-rebuild build-image` works similar to the popular [nix-community/nixos-generators](https://github.com/nix-community/nixos-generators) project. See new [section on image building in the NixOS manual](https://nixos.org/manual/nixos/unstable/#sec-image-nixos-rebuild-build-image). It is also available for `nixos-rebuild-ng`.
- `nixos-option` has been rewritten to a Nix expression called by a simple bash script. This lowers our maintenance threshold, makes eval errors less verbose, adds support for flake-based configurations, descending into `attrsOf` and `listOf` submodule options, and `--show-trace`.
@@ -108,12 +108,16 @@
- [Schroot](https://codeberg.org/shelter/reschroot), a lightweight virtualisation tool. Securely enter a chroot and run a command or login shell. Available as [programs.schroot](#opt-programs.schroot.enable).
- [Firezone](https://firezone.dev), an enterprise-ready zero-trust access platform built on WireGuard. This includes the server stack as [services.firezone.server.enable](#opt-services.firezone.server.enable), a TURN/STUN relay service as [services.firezone.relay.enable](#opt-services.firezone.relay.enable), a gateway service as [services.firezone.gateway.enable](#opt-services.firezone.gateway.enable), a headless client as [services.firezone.headless-client.enable](#opt-services.firezone.headless-client.enable) and a GUI client as [services.firezone.gui-client.enable](#opt-services.firezone.gui-client.enable).
- [crab-hole](https://github.com/LuckyTurtleDev/crab-hole), a cross platform Pi-hole clone written in Rust using hickory-dns/trust-dns. Available as [services.crab-hole](#opt-services.crab-hole.enable).
- [zwave-js-ui](https://zwave-js.github.io/zwave-js-ui/), a full featured Z-Wave Control Panel and MQTT Gateway. Available as [services.zwave-js-ui](#opt-services.zwave-js-ui.enable).
- [Amazon CloudWatch Agent](https://github.com/aws/amazon-cloudwatch-agent), the official telemetry collector for AWS CloudWatch and AWS X-Ray. Available as [services.amazon-cloudwatch-agent](options.html#opt-services.amazon-cloudwatch-agent.enable).
- [Fluent Bit](https://github.com/fluent/fluent-bit), a fast Log, Metrics and Traces Processor and Forwarder. Available as [services.fluent-bit](#opt-services.fluent-bit.enable).
- [Bat](https://github.com/sharkdp/bat), a {manpage}`cat(1)` clone with wings. Available as [programs.bat](options.html#opt-programs.bat).
- [Autotier](https://github.com/45Drives/autotier), a passthrough FUSE filesystem. Available as [services.autotierfs](options.html#opt-services.autotierfs.enable).
@@ -214,6 +218,8 @@
- `python3Packages.beancount` was updated to 3.1.0. Previous major version remains available as `python3Packages.beancount_2`.
- `wastebin` has been updated to 3.0.0. See the [Changelog](https://github.com/matze/wastebin/blob/master/CHANGELOG.md#300) for breaking changes to the configuration.
- `binwalk` was updated to 3.1.0, which has been rewritten in rust. The python module is no longer available.
See the release notes of [3.1.0](https://github.com/ReFirmLabs/binwalk/releases/tag/v3.1.0) for more information.
@@ -225,6 +231,11 @@
- `pkgs.nextcloud28` has been removed since it's out of support upstream.
- `services.cloudflare-dyndns.apiTokenFile` now must be just your Cloudflare api token. Previously it was supposed to be a file of the form `CLOUDFLARE_API_TOKEN=...`.
- Emacs lisp build helpers, such as `emacs.pkgs.melpaBuild`, now enables `__structuredAttrs` by default.
Environment variables have to be passed via the `env` attribute.
- `buildGoModule` now passes environment variables via the `env` attribute. `CGO_ENABLED` should now be specified with `env.CGO_ENABLED` when passing to buildGoModule. Direct specification of `CGO_ENABLED` is now redirected by a compatibility layer with a warning, but will become an error in future releases.
Go-related environment variables previously shadowed by `buildGoModule` now results in errors when specified directly. Such variables include `GOOS` and `GOARCH`.
@@ -603,6 +614,8 @@
- `services.soft-serve` now restarts upon config change.
- `services.keycloak` now provides a `realmFiles` option that allows to import realms during startup. See https://www.keycloak.org/server/importExport
- `bind.cacheNetworks` now only controls access for recursive queries, where it previously controlled access for all queries.
- [`services.mongodb.enableAuth`](#opt-services.mongodb.enableAuth) now uses the newer [mongosh](https://github.com/mongodb-js/mongosh) shell instead of the legacy shell to configure the initial superuser. You can configure the mongosh package to use through the [`services.mongodb.mongoshPackage`](#opt-services.mongodb.mongoshPackage) option.
+1 -1
View File
@@ -58,7 +58,7 @@ let
<fontconfig>
<!-- Font directories -->
${lib.concatStringsSep "\n" (map (font: "<dir>${font}</dir>") config.fonts.packages)}
${lib.optionalString (pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) ''
${lib.optionalString (pkgs.stdenv.hostPlatform.emulatorAvailable pkgs.buildPackages) ''
<!-- Pre-generated font caches -->
<cachedir>${cache}</cachedir>
${lib.optionalString (pkgs.stdenv.hostPlatform.isx86_64 && cfg.cache32Bit) ''
+1 -1
View File
@@ -376,4 +376,4 @@ foreach my $u (values %usersOut) {
updateFile("/etc/subuid", join("\n", @subUids) . "\n");
updateFile("/etc/subgid", join("\n", @subGids) . "\n");
updateFile($subUidMapFile, encode_json($subUidMap) . "\n");
updateFile($subUidMapFile, to_json($subUidMap) . "\n");
+15
View File
@@ -951,6 +951,21 @@ in {
}
] ++ flatten (flip mapAttrsToList cfg.users (name: user:
[
(
let
# Things fail in various ways with especially non-ascii usernames.
# This regex mirrors the one from shadow's is_valid_name:
# https://github.com/shadow-maint/shadow/blob/bee77ffc291dfed2a133496db465eaa55e2b0fec/lib/chkname.c#L68
# though without the trailing $, because Samba 3 got its last release
# over 10 years ago and is not in Nixpkgs anymore,
# while later versions don't appear to require anything like that.
nameRegex = "[a-zA-Z0-9_.][a-zA-Z0-9_.-]*";
in
{
assertion = builtins.match nameRegex user.name != null;
message = "The username \"${user.name}\" is not valid, it does not match the regex \"${nameRegex}\".";
}
)
{
assertion = (user.hashedPassword != null)
-> (match ".*:.*" user.hashedPassword == null);
+4 -1
View File
@@ -47,7 +47,10 @@ let
let
module = ../. + "/installer/sd-card/sd-image-${pkgs.targetPlatform.qemuArch}.nix";
in
if builtins.pathExists module then [ module ] else throw "The module ${module} does not exist.";
if builtins.pathExists module then
[ module ]
else
throw "The module ${toString module} does not exist.";
};
kexec = ../installer/netboot/netboot-minimal.nix;
};
+8
View File
@@ -560,6 +560,7 @@
./services/desktops/system76-scheduler.nix
./services/desktops/telepathy.nix
./services/desktops/tumbler.nix
./services/desktops/wlock.nix
./services/desktops/zeitgeist.nix
./services/development/athens.nix
./services/development/blackfire.nix
@@ -934,6 +935,7 @@
./services/monitoring/das_watchdog.nix
./services/monitoring/datadog-agent.nix
./services/monitoring/do-agent.nix
./services/monitoring/fluent-bit.nix
./services/monitoring/fusion-inventory.nix
./services/monitoring/gatus.nix
./services/monitoring/gitwatch.nix
@@ -1101,6 +1103,11 @@
./services/networking/firewall.nix
./services/networking/firewall-iptables.nix
./services/networking/firewall-nftables.nix
./services/networking/firezone/gateway.nix
./services/networking/firezone/gui-client.nix
./services/networking/firezone/headless-client.nix
./services/networking/firezone/relay.nix
./services/networking/firezone/server.nix
./services/networking/flannel.nix
./services/networking/freenet.nix
./services/networking/freeradius.nix
@@ -1364,6 +1371,7 @@
./services/scheduling/atd.nix
./services/scheduling/cron.nix
./services/scheduling/fcron.nix
./services/scheduling/prefect.nix
./services/scheduling/scx.nix
./services/search/elasticsearch-curator.nix
./services/search/elasticsearch.nix
@@ -102,10 +102,10 @@ with lib;
stdenv
stdenvNoCC # for runCommand
busybox
jq # for closureInfo
# For boot.initrd.systemd
makeInitrdNGTool
];
]
++ jq.all; # for closureInfo
boot.swraid.enable = true;
# remove warning about unset mail
+7
View File
@@ -118,8 +118,15 @@ in
Some of these might be able to be configured more ergonomically
using policies.
See [here](https://mozilla.github.io/policy-templates/#preferences) for allowed preferences.
${organisationInfo}
'';
example = lib.literalExpression ''
{
"browser.tabs.tabmanager.enabled" = false;
}
'';
};
preferencesStatus = lib.mkOption {
+8 -1
View File
@@ -10,7 +10,14 @@ in
{
options.programs.streamcontroller = {
enable = lib.mkEnableOption "StreamController";
package = lib.mkPackageOption pkgs "streamcontroller" { default = [ "streamcontroller" ]; };
package = lib.mkOption {
default = pkgs.streamcontroller.override { isKde = config.services.desktopManager.plasma6.enable; };
defaultText = lib.literalExpression "pkgs.streamcontroller";
type = lib.types.package;
description = ''
The StreamController package to use
'';
};
};
config = lib.mkIf cfg.enable {
+1 -2
View File
@@ -79,8 +79,7 @@ in
description = ''
The package which contains the `yabar` binary.
Nixpkgs provides the `yabar` and `yabar-unstable`,
so it's possible to choose.
Nixpkgs offers both a stable (`yabar`) and unstable (`yabar-unstable`) version of Yabar.
'';
};
@@ -202,8 +202,9 @@ in
systemd.user.services =
let
lomiriService = "lomiri.service";
lomiriServiceNames = [
"lomiri.service"
lomiriService
"lomiri-full-greeter.service"
"lomiri-full-shell.service"
"lomiri-greeter.service"
@@ -225,9 +226,9 @@ in
"lomiri-polkit-agent" = {
description = "Lomiri Polkit agent";
wantedBy = lomiriServiceNames;
after = [ "graphical-session.target" ];
partOf = lomiriServiceNames;
wantedBy = [ lomiriService ];
after = [ lomiriService ];
partOf = [ lomiriService ];
serviceConfig = {
Type = "simple";
Restart = "always";
+31
View File
@@ -0,0 +1,31 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.wlock;
in
{
options = {
services.wlock = {
enable = lib.mkEnableOption "wlock, a Wayland sessionlocker using the ext-session-lock-v1 protocol";
package = lib.mkPackageOption pkgs "wlock" { };
};
};
config = lib.mkIf cfg.enable {
security.wrappers.wlock = {
owner = "root";
group = "root";
# mirror upstream chmod of 4755
setuid = true;
setgid = false;
source = lib.getExe cfg.package;
};
};
meta.maintainers = [ lib.maintainers.fliegendewurst ];
}
@@ -9,7 +9,8 @@ let
registrationFile = "${dataDir}/telegram-registration.yaml";
cfg = config.services.mautrix-telegram;
settingsFormat = pkgs.formats.json { };
settingsFile = settingsFormat.generate "mautrix-telegram-config.json" cfg.settings;
settingsFileUnsubstituted = settingsFormat.generate "mautrix-telegram-config.json" cfg.settings;
settingsFile = "${dataDir}/config.json";
in
{
@@ -132,10 +133,37 @@ in
List of Systemd services to require and wait for when starting the application service.
'';
};
registerToSynapse = lib.mkOption {
type = lib.types.bool;
default = config.services.matrix-synapse.enable;
defaultText = lib.literalExpression "config.services.matrix-synapse.enable";
description = ''
Whether to add the bridge's app service registration file to
`services.matrix-synapse.settings.app_service_config_files`.
'';
};
};
};
config = lib.mkIf cfg.enable {
users.users.mautrix-telegram = {
isSystemUser = true;
group = "mautrix-telegram";
home = dataDir;
description = "Mautrix-Telegram bridge user";
};
users.groups.mautrix-telegram = { };
services.matrix-synapse = lib.mkIf cfg.registerToSynapse {
settings.app_service_config_files = [ registrationFile ];
};
systemd.services.matrix-synapse = lib.mkIf cfg.registerToSynapse {
serviceConfig.SupplementaryGroups = [ "mautrix-telegram" ];
};
systemd.services.mautrix-telegram = {
description = "Mautrix-Telegram, a Matrix-Telegram hybrid puppeting/relaybot bridge.";
@@ -161,6 +189,16 @@ in
preStart =
''
# substitute the settings file by environment variables
# in this case read from EnvironmentFile
test -f '${settingsFile}' && rm -f '${settingsFile}'
old_umask=$(umask)
umask 0177
${pkgs.envsubst}/bin/envsubst \
-o '${settingsFile}' \
-i '${settingsFileUnsubstituted}'
umask $old_umask
# generate the appservice's registration file if absent
if [ ! -f '${registrationFile}' ]; then
${pkgs.mautrix-telegram}/bin/mautrix-telegram \
@@ -168,6 +206,19 @@ in
--config='${settingsFile}' \
--registration='${registrationFile}'
fi
old_umask=$(umask)
umask 0177
# 1. Overwrite registration tokens in config
# is set, set it as the login shared secret value for the configured
# homeserver domain.
${pkgs.yq}/bin/yq -s '.[0].appservice.as_token = .[1].as_token
| .[0].appservice.hs_token = .[1].hs_token
| .[0]' \
'${settingsFile}' '${registrationFile}' > '${settingsFile}.tmp'
mv '${settingsFile}.tmp' '${settingsFile}'
umask $old_umask
''
+ lib.optionalString (pkgs.mautrix-telegram ? alembic) ''
# run automatic database init and migration scripts
@@ -175,6 +226,8 @@ in
'';
serviceConfig = {
User = "mautrix-telegram";
Group = "mautrix-telegram";
Type = "simple";
Restart = "always";
@@ -184,7 +237,6 @@ in
ProtectKernelModules = true;
ProtectControlGroups = true;
DynamicUser = true;
PrivateTmp = true;
WorkingDirectory = pkgs.mautrix-telegram; # necessary for the database migration scripts to be found
StateDirectory = baseNameOf dataDir;
@@ -192,80 +192,54 @@ in
};
};
config =
let
# If homepage-dashboard is enabled, but none of the configuration values have been updated,
# then default to "unmanaged" configuration which is manually updated in
# var/lib/homepage-dashboard. This is to maintain backwards compatibility, and should be
# deprecated in a future release.
managedConfig =
!(
cfg.bookmarks == [ ]
&& cfg.customCSS == ""
&& cfg.customJS == ""
&& cfg.docker == { }
&& cfg.kubernetes == { }
&& cfg.services == [ ]
&& cfg.settings == { }
&& cfg.widgets == [ ]
);
configDir = if managedConfig then "/etc/homepage-dashboard" else "/var/lib/homepage-dashboard";
msg =
"using unmanaged configuration for homepage-dashboard is deprecated and will be removed"
+ " in 24.05. please see the NixOS documentation for `services.homepage-dashboard' and add"
+ " your bookmarks, services, widgets, and other configuration using the options provided.";
in
lib.mkIf cfg.enable {
warnings = lib.optional (!managedConfig) msg;
environment.etc = lib.mkIf managedConfig {
"homepage-dashboard/custom.css".text = cfg.customCSS;
"homepage-dashboard/custom.js".text = cfg.customJS;
"homepage-dashboard/bookmarks.yaml".source = settingsFormat.generate "bookmarks.yaml" cfg.bookmarks;
"homepage-dashboard/docker.yaml".source = settingsFormat.generate "docker.yaml" cfg.docker;
"homepage-dashboard/kubernetes.yaml".source =
settingsFormat.generate "kubernetes.yaml" cfg.kubernetes;
"homepage-dashboard/services.yaml".source = settingsFormat.generate "services.yaml" cfg.services;
"homepage-dashboard/settings.yaml".source = settingsFormat.generate "settings.yaml" cfg.settings;
"homepage-dashboard/widgets.yaml".source = settingsFormat.generate "widgets.yaml" cfg.widgets;
};
systemd.services.homepage-dashboard = {
description = "Homepage Dashboard";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
HOMEPAGE_CONFIG_DIR = configDir;
NIXPKGS_HOMEPAGE_CACHE_DIR = "/var/cache/homepage-dashboard";
PORT = toString cfg.listenPort;
LOG_TARGETS = lib.mkIf managedConfig "stdout";
};
serviceConfig = {
Type = "simple";
DynamicUser = true;
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
StateDirectory = lib.mkIf (!managedConfig) "homepage-dashboard";
CacheDirectory = "homepage-dashboard";
ExecStart = lib.getExe cfg.package;
Restart = "on-failure";
};
preStart = ''
# Related:
# * https://github.com/NixOS/nixpkgs/issues/346016 ("homepage-dashboard: cache dir is not cleared upon version upgrade")
# * https://github.com/gethomepage/homepage/discussions/4560 ("homepage NixOS package does not clear cache on upgrade leaving broken state")
# * https://github.com/vercel/next.js/discussions/58864 ("Feature Request: Allow configuration of cache dir")
rm -rf "$NIXPKGS_HOMEPAGE_CACHE_DIR"/*
'';
};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.listenPort ];
};
config = lib.mkIf cfg.enable {
environment.etc = {
"homepage-dashboard/custom.css".text = cfg.customCSS;
"homepage-dashboard/custom.js".text = cfg.customJS;
"homepage-dashboard/bookmarks.yaml".source = settingsFormat.generate "bookmarks.yaml" cfg.bookmarks;
"homepage-dashboard/docker.yaml".source = settingsFormat.generate "docker.yaml" cfg.docker;
"homepage-dashboard/kubernetes.yaml".source =
settingsFormat.generate "kubernetes.yaml" cfg.kubernetes;
"homepage-dashboard/services.yaml".source = settingsFormat.generate "services.yaml" cfg.services;
"homepage-dashboard/settings.yaml".source = settingsFormat.generate "settings.yaml" cfg.settings;
"homepage-dashboard/widgets.yaml".source = settingsFormat.generate "widgets.yaml" cfg.widgets;
};
systemd.services.homepage-dashboard = {
description = "Homepage Dashboard";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
HOMEPAGE_CONFIG_DIR = "/etc/homepage-dashboard";
NIXPKGS_HOMEPAGE_CACHE_DIR = "/var/cache/homepage-dashboard";
PORT = toString cfg.listenPort;
LOG_TARGETS = "stdout";
};
serviceConfig = {
Type = "simple";
DynamicUser = true;
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
StateDirectory = "homepage-dashboard";
CacheDirectory = "homepage-dashboard";
ExecStart = lib.getExe cfg.package;
Restart = "on-failure";
};
enableStrictShellChecks = true;
# Related:
# * https://github.com/NixOS/nixpkgs/issues/346016 ("homepage-dashboard: cache dir is not cleared upon version upgrade")
# * https://github.com/gethomepage/homepage/discussions/4560 ("homepage NixOS package does not clear cache on upgrade leaving broken state")
# * https://github.com/vercel/next.js/discussions/58864 ("Feature Request: Allow configuration of cache dir")
preStart = ''
rm -rf "''${NIXPKGS_HOMEPAGE_CACHE_DIR:?}"/*
'';
};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.listenPort ];
};
};
}
+1 -1
View File
@@ -83,7 +83,7 @@ in
};
config = lib.mkIf cfg.enable {
services.orthanc.settings = options.services.orthanc.settings.default;
services.orthanc.settings = opt.settings.default;
systemd.services.orthanc = {
description = "Orthanc is a lightweight, RESTful DICOM server for healthcare and medical research";
@@ -0,0 +1,103 @@
{
config,
lib,
pkgs,
utils,
...
}:
let
cfg = config.services.fluent-bit;
yamlFormat = pkgs.formats.yaml { };
in
{
options.services.fluent-bit = {
enable = lib.mkEnableOption "Fluent Bit";
package = lib.mkPackageOption pkgs "fluent-bit" { };
configurationFile = lib.mkOption {
type = lib.types.path;
default = yamlFormat.generate "fluent-bit.yaml" cfg.settings;
defaultText = lib.literalExpression ''yamlFormat.generate "fluent-bit.yaml" cfg.settings'';
description = ''
Fluent Bit configuration. See
<https://docs.fluentbit.io/manual/administration/configuring-fluent-bit/yaml>
for supported values.
{option}`configurationFile` takes precedence over {option}`settings`.
Note: Restricted evaluation blocks access to paths outside the Nix store.
This means detecting content changes for mutable paths (i.e. not input or content-addressed) can't be done.
As a result, `nixos-rebuild` won't reload/restart the systemd unit when mutable path contents change.
`systemctl restart fluent-bit.service` must be used instead.
'';
example = "/etc/fluent-bit/fluent-bit.yaml";
};
settings = lib.mkOption {
type = yamlFormat.type;
default = { };
description = ''
See {option}`configurationFile`.
{option}`configurationFile` takes precedence over {option}`settings`.
'';
example = {
service = {
grace = 30;
};
pipeline = {
inputs = [
{
name = "systemd";
systemd_filter = "_SYSTEMD_UNIT=fluent-bit.service";
}
];
outputs = [
{
name = "file";
path = "/var/log/fluent-bit";
file = "fluent-bit.out";
}
];
};
};
};
# See https://docs.fluentbit.io/manual/administration/configuring-fluent-bit/yaml/service-section.
graceLimit = lib.mkOption {
type = lib.types.nullOr (
lib.types.oneOf [
lib.types.ints.positive
lib.types.str
]
);
default = null;
description = ''
The grace time limit. Sets the systemd unit's `TimeoutStopSec`.
The `service.grace` option in the Fluent Bit configuration should be this option.
'';
example = 30;
};
};
config = lib.mkIf cfg.enable {
# See https://github.com/fluent/fluent-bit/blob/v3.2.6/init/systemd.in.
systemd.services.fluent-bit = {
description = "Fluent Bit";
after = [ "network.target" ];
requires = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
DynamicUser = true;
# See https://nixos.org/manual/nixos/stable#sec-logging.
SupplementaryGroups = "systemd-journal";
ExecStart = utils.escapeSystemdExecArgs [
(lib.getExe cfg.package)
"--config"
cfg.configurationFile
];
Restart = "always";
TimeoutStopSec = lib.mkIf (cfg.graceLimit != null) cfg.graceLimit;
};
};
};
}
+24 -3
View File
@@ -1,10 +1,11 @@
{ config, lib, pkgs, ... }:
{ config, lib, pkgs, utils, ... }:
let
inherit (lib) maintainers;
inherit (lib.meta) getExe;
inherit (lib.modules) mkIf mkMerge;
inherit (lib.options) literalExpression mkEnableOption mkOption mkPackageOption;
inherit (lib.types) bool enum nullOr port str submodule;
inherit (utils) genJqSecretsReplacementSnippet;
cfg = config.services.scrutiny;
# Define the settings format used for this program
@@ -36,6 +37,11 @@ in
Scrutiny settings to be rendered into the configuration file.
See <https://github.com/AnalogJ/scrutiny/blob/master/example.scrutiny.yaml>.
Options containing secret data should be set to an attribute set
containing the attribute `_secret`. This attribute should be a string
or structured JSON with `quote = false;`, pointing to a file that
contains the value the option should be set to.
'';
default = { };
type = submodule {
@@ -130,6 +136,11 @@ in
Collector settings to be rendered into the collector configuration file.
See <https://github.com/AnalogJ/scrutiny/blob/master/example.collector.yaml>.
Options containing secret data should be set to an attribute set
containing the attribute `_secret`. This attribute should be a string
or structured JSON with `quote = false;`, pointing to a file that
contains the value the option should be set to.
'';
default = { };
type = submodule {
@@ -177,6 +188,9 @@ in
SCRUTINY_WEB_DATABASE_LOCATION = "/var/lib/scrutiny/scrutiny.db";
SCRUTINY_WEB_SRC_FRONTEND_PATH = "${cfg.package}/share/scrutiny";
};
preStart = ''
${genJqSecretsReplacementSnippet cfg.settings "/run/scrutiny/config.yaml"}
'';
postStart = ''
for i in $(seq 300); do
if "${lib.getExe pkgs.curl}" --fail --silent --head "http://${cfg.settings.web.listen.host}:${toString cfg.settings.web.listen.port}" >/dev/null; then
@@ -191,8 +205,10 @@ in
'';
serviceConfig = {
DynamicUser = true;
ExecStart = "${getExe cfg.package} start --config ${settingsFormat.generate "scrutiny.yaml" cfg.settings}";
ExecStart = "${getExe cfg.package} start --config /run/scrutiny/config.yaml";
Restart = "always";
RuntimeDirectory = "scrutiny";
RuntimeDirectoryMode = "0700";
StateDirectory = "scrutiny";
StateDirectoryMode = "0750";
};
@@ -216,9 +232,14 @@ in
COLLECTOR_VERSION = "1";
COLLECTOR_API_ENDPOINT = cfg.collector.settings.api.endpoint;
};
preStart = ''
${genJqSecretsReplacementSnippet cfg.collector.settings "/run/scrutiny-collector/config.yaml"}
'';
serviceConfig = {
Type = "oneshot";
ExecStart = "${getExe cfg.collector.package} run --config ${settingsFormat.generate "scrutiny-collector.yaml" cfg.collector.settings}";
ExecStart = "${getExe cfg.collector.package} run --config /run/scrutiny-collector/config.yaml";
RuntimeDirectory = "scrutiny-collector";
RuntimeDirectoryMode = "0700";
};
startAt = cfg.collector.schedule;
};
@@ -75,6 +75,7 @@ in
"AF_INET"
"AF_INET6"
"AF_NETLINK"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
@@ -15,12 +15,13 @@ in
package = lib.mkPackageOption pkgs "cloudflare-dyndns" { };
apiTokenFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
type = lib.types.pathWith {
absolute = true;
inStore = false;
};
description = ''
The path to a file containing the CloudFlare API token.
The file must have the form `CLOUDFLARE_API_TOKEN=...`
'';
};
@@ -91,19 +92,35 @@ in
Type = "simple";
DynamicUser = true;
StateDirectory = "cloudflare-dyndns";
EnvironmentFile = cfg.apiTokenFile;
Environment = [ "XDG_CACHE_HOME=%S/cloudflare-dyndns/.cache" ];
ExecStart =
let
args =
[ "--cache-file /var/lib/cloudflare-dyndns/ip.cache" ]
++ (if cfg.ipv4 then [ "-4" ] else [ "-no-4" ])
++ (if cfg.ipv6 then [ "-6" ] else [ "-no-6" ])
++ lib.optional cfg.deleteMissing "--delete-missing"
++ lib.optional cfg.proxied "--proxied";
in
"${lib.getExe cfg.package} ${toString args}";
LoadCredential = [
"apiToken:${cfg.apiTokenFile}"
];
};
script =
let
args =
[ "--cache-file /var/lib/cloudflare-dyndns/ip.cache" ]
++ (if cfg.ipv4 then [ "-4" ] else [ "-no-4" ])
++ (if cfg.ipv6 then [ "-6" ] else [ "-no-6" ])
++ lib.optional cfg.deleteMissing "--delete-missing"
++ lib.optional cfg.proxied "--proxied";
in
''
export CLOUDFLARE_API_TOKEN=$(< "''${CREDENTIALS_DIRECTORY}/apiToken")
# Added 2025-03-10: `cfg.apiTokenFile` used to be passed as an
# `EnvironmentFile` to the service, which required it to be of
# the form "CLOUDFLARE_API_TOKEN=" rather than just the secret.
# If we detect this legacy usage, error out.
if [[ $CLOUDFLARE_API_TOKEN == CLOUDFLARE_API_TOKEN* ]]; then
echo "Error: your api token starts with 'CLOUDFLARE_API_TOKEN='. Remove that, and instead specify just the token." >&2
exit 1
fi
exec ${lib.getExe cfg.package} ${toString args}
'';
}
// lib.optionalAttrs (cfg.frequency != null) {
startAt = cfg.frequency;
@@ -0,0 +1,159 @@
{
lib,
pkgs,
config,
...
}:
let
inherit (lib)
boolToString
getExe
mkEnableOption
mkIf
mkOption
mkPackageOption
types
;
cfg = config.services.firezone.gateway;
in
{
options = {
services.firezone.gateway = {
enable = mkOption {
default = false;
example = true;
description = ''
Whether to enable the firezone gateway.
You have to manually masquerade and forward traffic from the
tun-firezone interface to your resource! Refer to the
[upstream setup script](https://github.com/firezone/firezone/blob/8c7c0a9e8e33ae790aeb75fdb5a15432c2870b79/scripts/gateway-systemd-install.sh#L154-L168)
for a list of iptable commands.
See the firezone nixos test in this repository for an nftables based example.
'';
type = lib.types.bool;
};
package = mkPackageOption pkgs "firezone-gateway" { };
name = mkOption {
type = types.str;
description = "The name of this gateway as shown in firezone";
};
apiUrl = mkOption {
type = types.strMatching "^wss://.+/$";
example = "wss://firezone.example.com/api/";
description = ''
The URL of your firezone server's API. This should be the same
as your server's setting for {option}`services.firezone.server.settings.api.externalUrl`,
but with `wss://` instead of `https://`.
'';
};
tokenFile = mkOption {
type = types.path;
example = "/run/secrets/firezone-gateway-token";
description = ''
A file containing the firezone gateway token. Do not use a nix-store path here
as it will make the token publicly readable!
This file will be passed via systemd credentials, it should only be accessible
by the root user.
'';
};
logLevel = mkOption {
type = types.str;
default = "info";
description = ''
The log level for the firezone application. See
[RUST_LOG](https://docs.rs/env_logger/latest/env_logger/#enabling-logging)
for the format.
'';
};
enableTelemetry = mkEnableOption "telemetry";
};
};
config = mkIf cfg.enable {
systemd.services.firezone-gateway = {
description = "Gateway service for the Firezone zero-trust access platform";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = [ pkgs.util-linux ];
script = ''
# If FIREZONE_ID is not given by the user, use a persisted (or newly generated) uuid.
if [[ -z "''${FIREZONE_ID:-}" ]]; then
if [[ ! -e gateway_id ]]; then
uuidgen -r > gateway_id
fi
export FIREZONE_ID=$(< gateway_id)
fi
export FIREZONE_TOKEN=$(< "$CREDENTIALS_DIRECTORY/firezone-token")
exec ${getExe cfg.package}
'';
environment = {
FIREZONE_API_URL = cfg.apiUrl;
FIREZONE_NAME = cfg.name;
FIREZONE_NO_TELEMETRY = boolToString (!cfg.enableTelemetry);
RUST_LOG = cfg.logLevel;
};
serviceConfig = {
Type = "exec";
DynamicUser = true;
User = "firezone-gateway";
LoadCredential = [ "firezone-token:${cfg.tokenFile}" ];
DeviceAllow = "/dev/net/tun";
AmbientCapabilities = [ "CAP_NET_ADMIN" ];
CapabilityBoundingSet = [ "CAP_NET_ADMIN" ];
StateDirectory = "firezone-gateway";
WorkingDirectory = "/var/lib/firezone-gateway";
Restart = "on-failure";
RestartSec = 10;
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = false;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_NETLINK"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = "@system-service";
UMask = "077";
};
};
};
meta.maintainers = with lib.maintainers; [
oddlama
patrickdag
];
}
@@ -0,0 +1,138 @@
{
lib,
pkgs,
config,
...
}:
let
inherit (lib)
boolToString
getExe'
mkEnableOption
mkIf
mkOption
mkPackageOption
types
;
cfg = config.services.firezone.gui-client;
in
{
options = {
services.firezone.gui-client = {
enable = mkEnableOption "the firezone gui client";
package = mkPackageOption pkgs "firezone-gui-client" { };
allowedUsers = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
All listed users will become part of the `firezone-client` group so
they can control the IPC service. This is a convenience option.
'';
};
name = mkOption {
type = types.str;
description = "The name of this client as shown in firezone";
};
logLevel = mkOption {
type = types.str;
default = "info";
description = ''
The log level for the firezone application. See
[RUST_LOG](https://docs.rs/env_logger/latest/env_logger/#enabling-logging)
for the format.
'';
};
};
};
config = mkIf cfg.enable {
users.groups.firezone-client.members = cfg.allowedUsers;
# Required for deep-link mimetype registration
environment.systemPackages = [ cfg.package ];
# Required for the token store in the gui application
services.gnome.gnome-keyring.enable = true;
systemd.services.firezone-ipc-service = {
description = "GUI IPC service for the Firezone zero-trust access platform";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = [ pkgs.util-linux ];
script = ''
# If FIREZONE_ID is not given by the user, use a persisted (or newly generated) uuid.
if [[ -z "''${FIREZONE_ID:-}" ]]; then
if [[ ! -e client_id ]]; then
uuidgen -r > client_id
fi
export FIREZONE_ID=$(< client_id)
fi
exec ${getExe' cfg.package "firezone-client-ipc"} run
'';
environment = {
FIREZONE_NAME = cfg.name;
LOG_DIR = "%L/dev.firezone.client";
RUST_LOG = cfg.logLevel;
};
serviceConfig = {
Type = "notify";
DeviceAllow = "/dev/net/tun";
AmbientCapabilities = [ "CAP_NET_ADMIN" ];
CapabilityBoundingSet = [ "CAP_NET_ADMIN" ];
# This block contains hardcoded values in the client, we cannot change these :(
Group = "firezone-client";
RuntimeDirectory = "dev.firezone.client";
StateDirectory = "dev.firezone.client";
WorkingDirectory = "/var/lib/dev.firezone.client";
LogsDirectory = "dev.firezone.client";
Restart = "on-failure";
RestartSec = 10;
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = false;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_NETLINK"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = "@system-service";
UMask = "077";
};
};
};
meta.maintainers = with lib.maintainers; [
oddlama
patrickdag
];
}
@@ -0,0 +1,148 @@
{
lib,
pkgs,
config,
...
}:
let
inherit (lib)
boolToString
getExe
mkEnableOption
mkIf
mkOption
mkPackageOption
types
;
cfg = config.services.firezone.headless-client;
in
{
options = {
services.firezone.headless-client = {
enable = mkEnableOption "the firezone headless client";
package = mkPackageOption pkgs "firezone-headless-client" { };
name = mkOption {
type = types.str;
description = "The name of this client as shown in firezone";
};
apiUrl = mkOption {
type = types.strMatching "^wss://.+/$";
example = "wss://firezone.example.com/api/";
description = ''
The URL of your firezone server's API. This should be the same
as your server's setting for {option}`services.firezone.server.settings.api.externalUrl`,
but with `wss://` instead of `https://`.
'';
};
tokenFile = mkOption {
type = types.path;
example = "/run/secrets/firezone-client-token";
description = ''
A file containing the firezone client token. Do not use a nix-store path here
as it will make the token publicly readable!
This file will be passed via systemd credentials, it should only be accessible
by the root user.
'';
};
logLevel = mkOption {
type = types.str;
default = "info";
description = ''
The log level for the firezone application. See
[RUST_LOG](https://docs.rs/env_logger/latest/env_logger/#enabling-logging)
for the format.
'';
};
enableTelemetry = mkEnableOption "telemetry";
};
};
config = mkIf cfg.enable {
systemd.services.firezone-headless-client = {
description = "headless client service for the Firezone zero-trust access platform";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = [ pkgs.util-linux ];
script = ''
# If FIREZONE_ID is not given by the user, use a persisted (or newly generated) uuid.
if [[ -z "''${FIREZONE_ID:-}" ]]; then
if [[ ! -e client_id ]]; then
uuidgen -r > client_id
fi
export FIREZONE_ID=$(< client_id)
fi
exec ${getExe cfg.package}
'';
environment = {
FIREZONE_API_URL = cfg.apiUrl;
FIREZONE_NAME = cfg.name;
FIREZONE_NO_TELEMETRY = boolToString (!cfg.enableTelemetry);
FIREZONE_TOKEN_PATH = "%d/firezone-token";
LOG_DIR = "%L/dev.firezone.client";
RUST_LOG = cfg.logLevel;
};
serviceConfig = {
Type = "exec";
LoadCredential = [ "firezone-token:${cfg.tokenFile}" ];
DeviceAllow = "/dev/net/tun";
AmbientCapabilities = [ "CAP_NET_ADMIN" ];
CapabilityBoundingSet = [ "CAP_NET_ADMIN" ];
# Hardcoded values in the client :(
RuntimeDirectory = "dev.firezone.client";
StateDirectory = "dev.firezone.client";
WorkingDirectory = "/var/lib/dev.firezone.client";
LogsDirectory = "dev.firezone.client";
Restart = "on-failure";
RestartSec = 10;
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = false;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_NETLINK"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = "@system-service";
UMask = "077";
};
};
};
meta.maintainers = with lib.maintainers; [
oddlama
patrickdag
];
}
@@ -0,0 +1,709 @@
defmodule Provision do
alias Domain.{Repo, Accounts, Auth, Actors, Resources, Tokens, Gateways, Relays, Policies}
require Logger
# UUID Mapping handling
defmodule UuidMapping do
@mapping_file "provision-uuids.json"
# Loads the mapping from file
def load do
mappings = case File.read(@mapping_file) do
{:ok, content} ->
case Jason.decode(content) do
{:ok, mapping} -> mapping
_ -> %{"accounts" => %{}}
end
_ -> %{"accounts" => %{}}
end
Process.put(:uuid_mappings, mappings)
mappings
end
# Saves the current mapping (defaulting to the one in the process dictionary)
def save(mapping \\ Process.get(:uuid_mappings)) do
File.write!(@mapping_file, Jason.encode!(mapping))
end
# Retrieves the account-level mapping from a given mapping (or from Process)
def get_account(mapping \\ Process.get(:uuid_mappings), account_slug) do
get_in(mapping, ["accounts", account_slug]) || %{}
end
# Retrieves the entity mapping for a specific account and type
def get_entities(mapping \\ Process.get(:uuid_mappings), account_slug, type) do
get_in(mapping, ["accounts", account_slug, type]) || %{}
end
# Retrieves an entity mapping for a specific account, type and external_id
def get_entity(mapping \\ Process.get(:uuid_mappings), account_slug, type, external_id) do
get_in(mapping, ["accounts", account_slug, type, external_id])
end
# Updates (or creates) the account UUID mapping and stores it in the process dictionary.
def update_account(account_slug, uuid) do
mapping = Process.get(:uuid_mappings) || load()
mapping = ensure_account_exists(mapping, account_slug)
mapping = put_in(mapping, ["accounts", account_slug, "id"], uuid)
Process.put(:uuid_mappings, mapping)
mapping
end
# Ensures that the given account exists in the mapping.
def ensure_account_exists(mapping, account_slug) do
if not Map.has_key?(mapping["accounts"], account_slug) do
put_in(mapping, ["accounts", account_slug], %{})
else
mapping
end
end
# Updates (or creates) the mapping for entities of a given type for the account.
def update_entities(account_slug, type, new_entries) do
mapping = Process.get(:uuid_mappings) || load()
mapping = ensure_account_exists(mapping, account_slug)
current = get_entities(mapping, account_slug, type)
mapping = put_in(mapping, ["accounts", account_slug, type], Map.merge(current, new_entries))
Process.put(:uuid_mappings, mapping)
mapping
end
# Removes an entire account from the mapping.
def remove_account(account_slug) do
mapping = Process.get(:uuid_mappings) || load()
mapping = update_in(mapping, ["accounts"], fn accounts ->
Map.delete(accounts, account_slug)
end)
Process.put(:uuid_mappings, mapping)
mapping
end
# Removes a specific entity mapping for the account.
def remove_entity(account_slug, type, key) do
mapping = Process.get(:uuid_mappings) || load()
mapping = update_in(mapping, ["accounts", account_slug, type], fn entities ->
Map.delete(entities || %{}, key)
end)
Process.put(:uuid_mappings, mapping)
mapping
end
end
defp resolve_references(value) when is_map(value) do
Enum.into(value, %{}, fn {k, v} -> {k, resolve_references(v)} end)
end
defp resolve_references(value) when is_list(value) do
Enum.map(value, &resolve_references/1)
end
defp resolve_references(value) when is_binary(value) do
Regex.replace(~r/\{env:([^}]+)\}/, value, fn _, var ->
System.get_env(var) || raise "Environment variable #{var} not set"
end)
end
defp resolve_references(value), do: value
defp atomize_keys(map) when is_map(map) do
Enum.into(map, %{}, fn {k, v} ->
{
if(is_binary(k), do: String.to_atom(k), else: k),
if(is_map(v), do: atomize_keys(v), else: v)
}
end)
end
defp cleanup_account(uuid) do
case Accounts.fetch_account_by_id_or_slug(uuid) do
{:ok, value} when value.deleted_at == nil ->
Logger.info("Deleting removed account #{value.slug}")
value |> Ecto.Changeset.change(%{ deleted_at: DateTime.utc_now() }) |> Repo.update!()
_ -> :ok
end
end
defp cleanup_actor(uuid, subject) do
case Actors.fetch_actor_by_id(uuid, subject) do
{:ok, value} ->
Logger.info("Deleting removed actor #{value.name}")
{:ok, _} = Actors.delete_actor(value, subject)
_ -> :ok
end
end
defp cleanup_provider(uuid, subject) do
case Auth.fetch_provider_by_id(uuid, subject) do
{:ok, value} ->
Logger.info("Deleting removed provider #{value.name}")
{:ok, _} = Auth.delete_provider(value, subject)
_ -> :ok
end
end
defp cleanup_gateway_group(uuid, subject) do
case Gateways.fetch_group_by_id(uuid, subject) do
{:ok, value} ->
Logger.info("Deleting removed gateway group #{value.name}")
{:ok, _} = Gateways.delete_group(value, subject)
_ -> :ok
end
end
defp cleanup_relay_group(uuid, subject) do
case Relays.fetch_group_by_id(uuid, subject) do
{:ok, value} ->
Logger.info("Deleting removed relay group #{value.name}")
{:ok, _} = Relays.delete_group(value, subject)
_ -> :ok
end
end
defp cleanup_actor_group(uuid, subject) do
case Actors.fetch_group_by_id(uuid, subject) do
{:ok, value} ->
Logger.info("Deleting removed actor group #{value.name}")
{:ok, _} = Actors.delete_group(value, subject)
_ -> :ok
end
end
# Fetch resource by uuid, but follow the chain of replacements if any
defp fetch_resource(uuid, subject) do
case Resources.fetch_resource_by_id(uuid, subject) do
{:ok, resource} when resource.replaced_by_resource_id != nil -> fetch_resource(resource.replaced_by_resource_id, subject)
v -> v
end
end
defp cleanup_resource(uuid, subject) do
case fetch_resource(uuid, subject) do
{:ok, value} when value.deleted_at == nil ->
Logger.info("Deleting removed resource #{value.name}")
{:ok, _} = Resources.delete_resource(value, subject)
_ -> :ok
end
end
# Fetch policy by uuid, but follow the chain of replacements if any
defp fetch_policy(uuid, subject) do
case Policies.fetch_policy_by_id(uuid, subject) do
{:ok, policy} when policy.replaced_by_policy_id != nil -> fetch_policy(policy.replaced_by_policy_id, subject)
v -> v
end
end
defp cleanup_policy(uuid, subject) do
case fetch_policy(uuid, subject) do
{:ok, value} when value.deleted_at == nil ->
Logger.info("Deleting removed policy #{value.description}")
{:ok, _} = Policies.delete_policy(value, subject)
_ -> :ok
end
end
defp cleanup_entity_type(account_slug, entity_type, cleanup_fn, temp_admin_subject) do
# Get mapping for this entity type
existing_entities = UuidMapping.get_entities(account_slug, entity_type)
# Get current entities from account data
current_entities = Process.get(:current_entities)
# Determine which ones to remove
removed_entity_ids = Map.keys(existing_entities) -- (current_entities[entity_type] || [])
# Process each entity to remove
Enum.each(removed_entity_ids, fn entity_id ->
case existing_entities[entity_id] do
nil -> :ok
uuid ->
cleanup_fn.(uuid, temp_admin_subject)
UuidMapping.remove_entity(account_slug, entity_type, entity_id)
end
end)
end
defp collect_current_entities(account_data) do
%{
"actors" => Map.keys(account_data["actors"] || %{}),
"providers" => Map.keys(account_data["auth"] || %{}),
"gateway_groups" => Map.keys(account_data["gatewayGroups"] || %{}),
"relay_groups" => Map.keys(account_data["relayGroups"] || %{}),
"actor_groups" => Map.keys(account_data["groups"] || %{}) ++ ["everyone"],
"resources" => Map.keys(account_data["resources"] || %{}),
"policies" => Map.keys(account_data["policies"] || %{})
}
end
defp nil_if_deleted_or_not_found(value) do
case value do
nil -> nil
{:error, :not_found} -> nil
{:ok, value} when value.deleted_at != nil -> nil
v -> v
end
end
defp create_temp_admin(account, email_provider) do
temp_admin_actor_email = "firezone-provision@localhost.local"
temp_admin_actor_context = %Auth.Context{
type: :browser,
user_agent: "Unspecified/0.0",
remote_ip: {127, 0, 0, 1},
remote_ip_location_region: "N/A",
remote_ip_location_city: "N/A",
remote_ip_location_lat: 0.0,
remote_ip_location_lon: 0.0
}
{:ok, temp_admin_actor} =
Actors.create_actor(account, %{
type: :account_admin_user,
name: "Provisioning"
})
{:ok, temp_admin_actor_email_identity} =
Auth.create_identity(temp_admin_actor, email_provider, %{
provider_identifier: temp_admin_actor_email,
provider_identifier_confirmation: temp_admin_actor_email
})
{:ok, temp_admin_actor_token} =
Auth.create_token(temp_admin_actor_email_identity, temp_admin_actor_context, "temporarynonce", DateTime.utc_now() |> DateTime.add(1, :hour))
{:ok, temp_admin_subject} =
Auth.build_subject(temp_admin_actor_token, temp_admin_actor_context)
{temp_admin_subject, temp_admin_actor, temp_admin_actor_email_identity, temp_admin_actor_token}
end
defp cleanup_temp_admin(temp_admin_actor, temp_admin_actor_email_identity, temp_admin_actor_token, subject) do
Logger.info("Cleaning up temporary admin actor")
{:ok, _} = Tokens.delete_token(temp_admin_actor_token, subject)
{:ok, _} = Auth.delete_identity(temp_admin_actor_email_identity, subject)
{:ok, _} = Actors.delete_actor(temp_admin_actor, subject)
end
def provision() do
Logger.info("Starting provisioning")
# Load desired state
json_file = "provision-state.json"
{:ok, raw_json} = File.read(json_file)
{:ok, %{"accounts" => accounts}} = Jason.decode(raw_json)
accounts = resolve_references(accounts)
# Load existing UUID mappings into the process dictionary.
UuidMapping.load()
# Clean up removed accounts first
current_account_slugs = Map.keys(accounts)
existing_accounts = Map.keys(Process.get(:uuid_mappings)["accounts"])
removed_accounts = existing_accounts -- current_account_slugs
Enum.each(removed_accounts, fn slug ->
if uuid = get_in(Process.get(:uuid_mappings), ["accounts", slug, "id"]) do
cleanup_account(uuid)
# Remove the account from the UUID mapping.
UuidMapping.remove_account(slug)
end
end)
multi = Enum.reduce(accounts, Ecto.Multi.new(), fn {slug, account_data}, multi ->
account_attrs = atomize_keys(%{
name: account_data["name"],
slug: slug,
features: Map.get(account_data, "features", %{}),
metadata: Map.get(account_data, "metadata", %{}),
limits: Map.get(account_data, "limits", %{})
})
multi = multi
|> Ecto.Multi.run({:account, slug}, fn repo, _changes ->
case Accounts.fetch_account_by_id_or_slug(slug) do
{:ok, acc} ->
Logger.info("Updating existing account #{slug}")
updated_acc = acc |> Ecto.Changeset.change(account_attrs) |> repo.update!()
{:ok, {:existing, updated_acc}}
_ ->
Logger.info("Creating new account #{slug}")
{:ok, account} = Accounts.create_account(account_attrs)
Logger.info("Creating internet gateway group")
{:ok, internet_site} = Gateways.create_internet_group(account)
Logger.info("Creating internet resource")
{:ok, _internet_resource} = Resources.create_internet_resource(account, internet_site)
# Store mapping of slug to UUID
UuidMapping.update_account(slug, account.id)
{:ok, {:new, account}}
end
end)
|> Ecto.Multi.run({:everyone_group, slug}, fn _repo, changes ->
case Map.get(changes, {:account, slug}) do
{:new, account} ->
Logger.info("Creating everyone group for new account")
{:ok, actor_group} = Actors.create_managed_group(account, %{name: "Everyone", membership_rules: [%{operator: true}]})
UuidMapping.update_entities(slug, "actor_groups", %{"everyone" => actor_group.id})
{:ok, actor_group}
{:existing, _account} ->
{:ok, :skipped}
end
end)
|> Ecto.Multi.run({:email_provider, slug}, fn _repo, changes ->
case Map.get(changes, {:account, slug}) do
{:new, account} ->
Logger.info("Creating default email provider for new account")
Auth.create_provider(account, %{name: "Email", adapter: :email, adapter_config: %{}})
{:existing, account} ->
Auth.Provider.Query.not_disabled()
|> Auth.Provider.Query.by_adapter(:email)
|> Auth.Provider.Query.by_account_id(account.id)
|> Repo.fetch(Auth.Provider.Query, [])
end
end)
|> Ecto.Multi.run({:temp_admin, slug}, fn _repo, changes ->
{_, account} = changes[{:account, slug}]
email_provider = changes[{:email_provider, slug}]
{:ok, create_temp_admin(account, email_provider)}
end)
# Clean up removed entities for this account after we have an admin subject
multi = multi
|> Ecto.Multi.run({:cleanup_entities, slug}, fn _repo, changes ->
{temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
# Store current entities in process dictionary for our helper function
current_entities = collect_current_entities(account_data)
Process.put(:current_entities, current_entities)
# Define entity types and their cleanup functions
entity_types = [
{"actors", &cleanup_actor/2},
{"providers", &cleanup_provider/2},
{"gateway_groups", &cleanup_gateway_group/2},
{"relay_groups", &cleanup_relay_group/2},
{"actor_groups", &cleanup_actor_group/2},
{"resources", &cleanup_resource/2},
{"policies", &cleanup_policy/2}
]
# Clean up each entity type
Enum.each(entity_types, fn {entity_type, cleanup_fn} ->
cleanup_entity_type(slug, entity_type, cleanup_fn, temp_admin_subject)
end)
{:ok, :cleaned}
end)
# Create or update actors
multi = Enum.reduce(account_data["actors"] || %{}, multi, fn {external_id, actor_data}, multi ->
actor_attrs = atomize_keys(%{
name: actor_data["name"],
type: String.to_atom(actor_data["type"])
})
Ecto.Multi.run(multi, {:actor, slug, external_id}, fn _repo, changes ->
{_, account} = changes[{:account, slug}]
{temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
uuid = UuidMapping.get_entity(slug, "actors", external_id)
case uuid && Actors.fetch_actor_by_id(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do
nil ->
Logger.info("Creating new actor #{actor_data["name"]}")
{:ok, actor} = Actors.create_actor(account, actor_attrs)
# Update the mapping without manually handling Process.get/put.
UuidMapping.update_entities(slug, "actors", %{external_id => actor.id})
{:ok, {:new, actor}}
{:ok, existing_actor} ->
Logger.info("Updating existing actor #{actor_data["name"]}")
{:ok, updated_act} = Actors.update_actor(existing_actor, actor_attrs, temp_admin_subject)
{:ok, {:existing, updated_act}}
end
end)
|> Ecto.Multi.run({:actor_identity, slug, external_id}, fn repo, changes ->
email_provider = changes[{:email_provider, slug}]
case Map.get(changes, {:actor, slug, external_id}) do
{:new, actor} ->
Logger.info("Creating actor email identity")
Auth.create_identity(actor, email_provider, %{
provider_identifier: actor_data["email"],
provider_identifier_confirmation: actor_data["email"]
})
{:existing, actor} ->
Logger.info("Updating actor email identity")
{:ok, identity} = Auth.Identity.Query.not_deleted()
|> Auth.Identity.Query.by_actor_id(actor.id)
|> Auth.Identity.Query.by_provider_id(email_provider.id)
|> Repo.fetch(Auth.Identity.Query, [])
{:ok, identity |> Ecto.Changeset.change(%{
provider_identifier: actor_data["email"]
}) |> repo.update!()}
end
end)
end)
# Create or update providers
multi = Enum.reduce(account_data["auth"] || %{}, multi, fn {external_id, provider_data}, multi ->
Ecto.Multi.run(multi, {:provider, slug, external_id}, fn repo, changes ->
provider_attrs = %{
name: provider_data["name"],
adapter: String.to_atom(provider_data["adapter"]),
adapter_config: provider_data["adapter_config"]
}
{_, account} = changes[{:account, slug}]
{temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
uuid = UuidMapping.get_entity(slug, "providers", external_id)
case uuid && Auth.fetch_provider_by_id(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do
nil ->
Logger.info("Creating new provider #{provider_data["name"]}")
{:ok, provider} = Auth.create_provider(account, provider_attrs)
UuidMapping.update_entities(slug, "providers", %{external_id => provider.id})
{:ok, provider}
{:ok, existing} ->
Logger.info("Updating existing provider #{provider_data["name"]}")
{:ok, existing |> Ecto.Changeset.change(provider_attrs) |> repo.update!()}
end
end)
end)
# Create or update gateway_groups
multi = Enum.reduce(account_data["gatewayGroups"] || %{}, multi, fn {external_id, gateway_group_data}, multi ->
Ecto.Multi.run(multi, {:gateway_group, slug, external_id}, fn _repo, changes ->
gateway_group_attrs = %{
name: gateway_group_data["name"],
tokens: [%{}]
}
{_, account} = changes[{:account, slug}]
{temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
uuid = UuidMapping.get_entity(slug, "gateway_groups", external_id)
case uuid && Gateways.fetch_group_by_id(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do
nil ->
Logger.info("Creating new gateway group #{gateway_group_data["name"]}")
gateway_group = account
|> Gateways.Group.Changeset.create(gateway_group_attrs, temp_admin_subject)
|> Repo.insert!()
UuidMapping.update_entities(slug, "gateway_groups", %{external_id => gateway_group.id})
{:ok, gateway_group}
{:ok, existing} ->
# Nothing to update
{:ok, existing}
end
end)
end)
# Create or update relay_groups
multi = Enum.reduce(account_data["relayGroups"] || %{}, multi, fn {external_id, relay_group_data}, multi ->
Ecto.Multi.run(multi, {:relay_group, slug, external_id}, fn _repo, changes ->
relay_group_attrs = %{
name: relay_group_data["name"]
}
{temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
uuid = UuidMapping.get_entity(slug, "relay_groups", external_id)
existing_relay_group = uuid && Relays.fetch_group_by_id(uuid, temp_admin_subject)
case existing_relay_group do
v when v in [nil, {:error, :not_found}] ->
Logger.info("Creating new relay group #{relay_group_data["name"]}")
{:ok, relay_group} = Relays.create_group(relay_group_attrs, temp_admin_subject)
UuidMapping.update_entities(slug, "relay_groups", %{external_id => relay_group.id})
{:ok, relay_group}
{:ok, existing} ->
# Nothing to update
{:ok, existing}
end
end)
end)
# Create or update actor_groups
multi = Enum.reduce(account_data["groups"] || %{}, multi, fn {external_id, actor_group_data}, multi ->
Ecto.Multi.run(multi, {:actor_group, slug, external_id}, fn _repo, changes ->
actor_group_attrs = %{
name: actor_group_data["name"],
type: :static
}
{temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
uuid = UuidMapping.get_entity(slug, "actor_groups", external_id)
case uuid && Actors.fetch_group_by_id(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do
nil ->
Logger.info("Creating new actor group #{actor_group_data["name"]}")
{:ok, actor_group} = Actors.create_group(actor_group_attrs, temp_admin_subject)
UuidMapping.update_entities(slug, "actor_groups", %{external_id => actor_group.id})
{:ok, actor_group}
{:ok, existing} ->
# Nothing to update
{:ok, existing}
end
end)
|> Ecto.Multi.run({:actor_group_members, slug, external_id}, fn repo, changes ->
{_, account} = changes[{:account, slug}]
group_uuid = UuidMapping.get_entity(slug, "actor_groups", external_id)
memberships =
Actors.Membership.Query.all()
|> Actors.Membership.Query.by_group_id(group_uuid)
|> Actors.Membership.Query.returning_all()
|> Repo.all()
existing_members = Enum.map(memberships, fn membership -> membership.actor_id end)
desired_members = Enum.map(actor_group_data["members"] || [], fn member ->
uuid = UuidMapping.get_entity(slug, "actors", member)
if uuid == nil do
raise "Cannot find provisioned actor #{member} to add to group"
end
uuid
end)
missing_members = desired_members -- existing_members
untracked_members = existing_members -- desired_members
Logger.info("Updating members for actor group #{external_id}")
Enum.each(missing_members || [], fn actor_uuid ->
Logger.info("Adding member #{external_id}")
Actors.Membership.Changeset.upsert(account.id, %Actors.Membership{}, %{
group_id: group_uuid,
actor_id: actor_uuid
})
|> repo.insert!()
end)
if actor_group_data["forceMembers"] == true do
# Remove untracked members
to_delete = Enum.map(untracked_members, fn actor_uuid -> {group_uuid, actor_uuid} end)
if to_delete != [] do
Actors.Membership.Query.by_group_id_and_actor_id({:in, to_delete})
|> repo.delete_all()
end
end
{:ok, nil}
end)
end)
# Create or update resources
multi = Enum.reduce(account_data["resources"] || %{}, multi, fn {external_id, resource_data}, multi ->
Ecto.Multi.run(multi, {:resource, slug, external_id}, fn _repo, changes ->
resource_attrs = %{
type: String.to_atom(resource_data["type"]),
name: resource_data["name"],
address: resource_data["address"],
address_description: resource_data["address_description"],
connections: Enum.map(resource_data["gatewayGroups"] || [], fn group ->
%{gateway_group_id: UuidMapping.get_entity(slug, "gateway_groups", group)}
end),
filters: Enum.map(resource_data["filters"] || [], fn filter ->
%{
ports: filter["ports"] || [],
protocol: String.to_atom(filter["protocol"])
}
end)
}
{temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
uuid = UuidMapping.get_entity(slug, "resources", external_id)
case uuid && fetch_resource(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do
nil ->
Logger.info("Creating new resource #{resource_data["name"]}")
{:ok, resource} = Resources.create_resource(resource_attrs, temp_admin_subject)
UuidMapping.update_entities(slug, "resources", %{external_id => resource.id})
{:ok, resource}
{:ok, existing} ->
existing = Repo.preload(existing, :connections)
Logger.info("Updating existing resource #{resource_data["name"]}")
only_updated_attrs = resource_attrs
|> Enum.reject(fn {key, value} ->
case key do
# Compare connections by gateway_group_id only
:connections -> value == Enum.map(existing.connections || [], fn conn -> Map.take(conn, [:gateway_group_id]) end)
# Compare filters by ports and protocol only
:filters -> value == Enum.map(existing.filters || [], fn filter -> Map.take(filter, [:ports, :protocol]) end)
_ -> Map.get(existing, key) == value
end
end)
|> Enum.into(%{})
if only_updated_attrs == %{} do
{:ok, existing}
else
resource = case existing |> Resources.update_resource(resource_attrs, temp_admin_subject) do
{:replaced, _old, new} ->
UuidMapping.update_entities(slug, "resources", %{external_id => new.id})
new
{:updated, value} -> value
x -> x
end
{:ok, resource}
end
end
end)
end)
# Create or update policies
multi = Enum.reduce(account_data["policies"] || %{}, multi, fn {external_id, policy_data}, multi ->
Ecto.Multi.run(multi, {:policy, slug, external_id}, fn _repo, changes ->
policy_attrs = %{
description: policy_data["description"],
actor_group_id: UuidMapping.get_entity(slug, "actor_groups", policy_data["group"]),
resource_id: UuidMapping.get_entity(slug, "resources", policy_data["resource"])
}
{temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
uuid = UuidMapping.get_entity(slug, "policies", external_id)
case uuid && fetch_policy(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do
nil ->
Logger.info("Creating new policy #{policy_data["name"]}")
{:ok, policy} = Policies.create_policy(policy_attrs, temp_admin_subject)
UuidMapping.update_entities(slug, "policies", %{external_id => policy.id})
{:ok, policy}
{:ok, existing} ->
Logger.info("Updating existing policy #{policy_data["name"]}")
only_updated_attrs = policy_attrs
|> Enum.reject(fn {key, value} -> Map.get(existing, key) == value end)
|> Enum.into(%{})
if only_updated_attrs == %{} do
{:ok, existing}
else
policy = case existing |> Policies.update_policy(policy_attrs, temp_admin_subject) do
{:replaced, _old, new} ->
UuidMapping.update_entities(slug, "policies", %{external_id => new.id})
new
{:updated, value} -> value
x -> x
end
{:ok, policy}
end
end
end)
end)
# Clean up temporary admin after all operations
multi |> Ecto.Multi.run({:cleanup_temp_admin, slug}, fn _repo, changes ->
{temp_admin_subject, temp_admin_actor, temp_admin_actor_email_identity, temp_admin_actor_token} =
changes[{:temp_admin, slug}]
cleanup_temp_admin(temp_admin_actor, temp_admin_actor_email_identity, temp_admin_actor_token, temp_admin_subject)
{:ok, :cleaned}
end)
end)
|> Ecto.Multi.run({:save_state}, fn _repo, _changes ->
# Save all UUID mappings to disk.
UuidMapping.save()
{:ok, :saved}
end)
case Repo.transaction(multi) do
{:ok, _result} ->
Logger.info("Provisioning completed successfully")
{:error, step, reason, _changes} ->
Logger.error("Provisioning failed at step #{inspect(step)}, no changes were applied: #{inspect(reason)}")
end
end
end
Provision.provision()
@@ -0,0 +1,202 @@
{
lib,
pkgs,
config,
...
}:
let
inherit (lib)
boolToString
getExe
mkEnableOption
mkIf
mkOption
mkPackageOption
types
;
cfg = config.services.firezone.relay;
in
{
options = {
services.firezone.relay = {
enable = mkEnableOption "the firezone relay server";
package = mkPackageOption pkgs "firezone-relay" { };
name = mkOption {
type = types.str;
example = "My relay";
description = "The name of this gateway as shown in firezone";
};
publicIpv4 = mkOption {
type = types.nullOr types.str;
default = null;
description = "The public ipv4 address of this relay";
};
publicIpv6 = mkOption {
type = types.nullOr types.str;
default = null;
description = "The public ipv6 address of this relay";
};
openFirewall = mkOption {
type = types.bool;
default = true;
description = "Opens up the main STUN port and the TURN allocation range.";
};
port = mkOption {
type = types.port;
default = 3478;
description = "The port to listen on for STUN messages";
};
lowestPort = mkOption {
type = types.port;
default = 49152;
description = "The lowest port to use in TURN allocation";
};
highestPort = mkOption {
type = types.port;
default = 65535;
description = "The highest port to use in TURN allocation";
};
apiUrl = mkOption {
type = types.strMatching "^wss://.+/$";
example = "wss://firezone.example.com/api/";
description = ''
The URL of your firezone server's API. This should be the same
as your server's setting for {option}`services.firezone.server.settings.api.externalUrl`,
but with `wss://` instead of `https://`.
'';
};
tokenFile = mkOption {
type = types.path;
example = "/run/secrets/firezone-relay-token";
description = ''
A file containing the firezone relay token. Do not use a nix-store path here
as it will make the token publicly readable!
This file will be passed via systemd credentials, it should only be accessible
by the root user.
'';
};
logLevel = mkOption {
type = types.str;
default = "info";
description = ''
The log level for the firezone application. See
[RUST_LOG](https://docs.rs/env_logger/latest/env_logger/#enabling-logging)
for the format.
'';
};
enableTelemetry = mkEnableOption "telemetry";
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = cfg.publicIpv4 != null || cfg.publicIpv6 != null;
message = "At least one of `services.firezone.relay.publicIpv4` and `services.firezone.relay.publicIpv6` must be set";
}
];
networking.firewall.allowedUDPPorts = mkIf cfg.openFirewall [ cfg.port ];
networking.firewall.allowedUDPPortRanges = mkIf cfg.openFirewall [
{
from = cfg.lowestPort;
to = cfg.highestPort;
}
];
systemd.services.firezone-relay = {
description = "relay service for the Firezone zero-trust access platform";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = [ pkgs.util-linux ];
script = ''
# If FIREZONE_ID is not given by the user, use a persisted (or newly generated) uuid.
if [[ -z "''${FIREZONE_ID:-}" ]]; then
if [[ ! -e relay_id ]]; then
uuidgen -r > relay_id
fi
export FIREZONE_ID=$(< relay_id)
fi
export FIREZONE_TOKEN=$(< "$CREDENTIALS_DIRECTORY/firezone-token")
exec ${getExe cfg.package}
'';
environment = {
FIREZONE_API_URL = cfg.apiUrl;
FIREZONE_NAME = cfg.name;
FIREZONE_TELEMETRY = boolToString cfg.enableTelemetry;
PUBLIC_IP4_ADDR = cfg.publicIpv4;
PUBLIC_IP6_ADDR = cfg.publicIpv6;
LISTEN_PORT = toString cfg.port;
LOWEST_PORT = toString cfg.lowestPort;
HIGHEST_PORT = toString cfg.highestPort;
RUST_LOG = cfg.logLevel;
LOG_FORMAT = "human";
};
serviceConfig = {
Type = "exec";
DynamicUser = true;
User = "firezone-relay";
LoadCredential = [ "firezone-token:${cfg.tokenFile}" ];
StateDirectory = "firezone-relay";
WorkingDirectory = "/var/lib/firezone-relay";
Restart = "on-failure";
RestartSec = 10;
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = false;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_NETLINK"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = "@system-service";
UMask = "077";
};
};
};
meta.maintainers = with lib.maintainers; [
oddlama
patrickdag
];
}
File diff suppressed because it is too large Load Diff
@@ -13,14 +13,13 @@ let
'';
};
zone_type = mkOption {
type = types.enum [ "Primary" "Secondary" "Hint" "Forward" ];
type = types.enum [ "Primary" "Secondary" "External" ];
default = "Primary";
description = ''
One of:
- "Primary" (the master, authority for the zone).
- "Secondary" (the slave, replicated from the primary).
- "Hint" (a cached zone with recursive resolver abilities).
- "Forward" (a cached zone where all requests are forwarded to another resolver).
- "External" (a cached zone that queries other nameservers).
For more details about these zone types, consult the documentation for BIND,
though note that hickory-dns supports only a subset of BIND's zone types:
@@ -28,13 +27,13 @@ let
'';
};
file = mkOption {
type = types.either types.path types.str;
default = "${config.zone}.zone";
defaultText = literalExpression ''"''${config.zone}.zone"'';
type = types.nullOr (types.either types.path types.str);
default = if config.zone_type != "External" then "${config.zone}.zone" else null;
defaultText = literalExpression ''if config.zone_type != "External" then "''${config.zone}.zone" else null'';
description = ''
Path to the .zone file.
If not fully-qualified, this path will be interpreted relative to the `directory` option.
If omitted, defaults to the value of the `zone` option suffixed with ".zone".
If omitted, defaults to the value of the `zone` option suffixed with ".zone" when `zone_type` isn't External; otherwise, defaults to `null`.
'';
};
};
@@ -82,7 +81,7 @@ in
configFile = mkOption {
type = types.path;
default = toml.generate "hickory-dns.toml" (
lib.filterAttrsRecursive (_: v: v != null) cfg.settings
lib.mapAttrs (_: v: if builtins.isList v then map (v: if builtins.isAttrs v then lib.filterAttrs (_: v: v != null) v else v) v else v) (lib.filterAttrsRecursive (_: v: v != null) cfg.settings)
);
defaultText = lib.literalExpression ''
let toml = pkgs.formats.toml { }; in toml.generate "hickory-dns.toml" cfg.settings
@@ -34,7 +34,7 @@ in
mode = "0600";
};
environment.systemPackages = with pkgs; [ targetcli ];
environment.systemPackages = with pkgs; [ targetcli-fb ];
boot.kernelModules = [
"configfs"
@@ -52,8 +52,8 @@ in
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.python3.pkgs.rtslib}/bin/targetctl restore";
ExecStop = "${pkgs.python3.pkgs.rtslib}/bin/targetctl clear";
ExecStart = "${lib.getExe pkgs.python3Packages.rtslib-fb} restore";
ExecStop = "${lib.getExe pkgs.python3Packages.rtslib-fb} clear";
RemainAfterExit = "yes";
};
};
@@ -119,6 +119,14 @@ in
are mutually exclusive.
'';
}
{
assertion = cfg.config != "" || (cfg.settings ? auth && cfg.settings.auth ? type);
message = ''
Radicale 3.5.0 changed the default value for `auth.type` from `none` to `denyall`.
You probably don't want `denyall`, so please set this explicitly.
https://github.com/Kozea/Radicale/blob/v3.5.0/CHANGELOG.md
'';
}
];
warnings =
@@ -5,7 +5,8 @@ with lib;
let
cfg = config.services.shairport-sync;
configFormat = pkgs.formats.libconfig {};
configFile = configFormat.generate "shairport-sync.conf" cfg.settings;
in
{
@@ -29,9 +30,44 @@ in
package = lib.options.mkPackageOption pkgs "shairport-sync" { };
settings = mkOption {
type = configFormat.type;
default = {
general.output_backend = "pa";
diagnostics.log_verbosity = 1;
};
example = {
general = {
name = "NixOS Shairport";
output_backend = "pw";
};
metadata = {
enabled = "yes";
include_cover_art = "yes";
cover_art_cache_directory = "/tmp/shairport-sync/.cache/coverart";
pipe_name = "/tmp/shairport-sync-metadata";
pipe_timeout = 5000;
};
mqtt = {
enabled = "yes";
hostname = "mqtt.server.domain.example";
port = 1883;
publish_parsed = "yes";
publish_cover = "yes";
};
};
description = ''
Configuration options for Shairport-Sync.
See the example [shairport-sync.conf][example-file] for possible options.
[example-file]: https://github.com/mikebrady/shairport-sync/blob/master/scripts/shairport-sync.conf
'';
};
arguments = mkOption {
type = types.str;
default = "-v -o pa";
default = "";
description = ''
Arguments to pass to the daemon. Defaults to a local pulseaudio
server.
@@ -77,6 +113,11 @@ in
services.avahi.publish.enable = true;
services.avahi.publish.userServices = true;
services.shairport-sync.settings = {
general.output_backend = lib.mkDefault "pa";
diagnostics.log_verbosity = lib.mkDefault 1;
};
users = {
users.${cfg.user} = {
description = "Shairport user";
@@ -108,8 +149,10 @@ in
};
};
environment.systemPackages = [ cfg.package ];
environment = {
systemPackages = [ cfg.package ];
etc."shairport-sync.conf".source = configFile;
};
};
}
@@ -62,7 +62,7 @@ let
>"$RUNTIME_DIRECTORY/api_key"
do sleep 1; done
(printf "X-API-Key: "; cat "$RUNTIME_DIRECTORY/api_key") >"$RUNTIME_DIRECTORY/headers"
${pkgs.curl}/bin/curl --fail -sSLk -H "@$RUNTIME_DIRECTORY/headers" \
${pkgs.curl}/bin/curl -sSLk -H "@$RUNTIME_DIRECTORY/headers" \
--retry 1000 --retry-delay 1 --retry-all-errors \
"$@"
}
@@ -45,6 +45,7 @@ let
}
// (optionalAttrs (cfg'.user == null) {
DynamicUser = true;
ProtectHome = true;
})
// (optionalAttrs (cfg'.additionalGroups != [ ]) {
SupplementaryGroups = cfg'.additionalGroups;
@@ -63,7 +64,6 @@ let
PrivateTmp = true;
NoNewPrivileges = true;
ProtectSystem = "strict";
ProtectHome = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
@@ -0,0 +1,232 @@
{
lib,
pkgs,
config,
...
}:
let
cfg = config.services.prefect;
inherit (lib.types)
bool
str
enum
path
attrsOf
nullOr
submodule
port
;
in
{
options.services.prefect = {
enable = lib.mkOption {
type = bool;
default = false;
description = "enable prefect server and worker services";
};
package = lib.mkPackageOption pkgs "prefect" { };
host = lib.mkOption {
type = str;
default = "127.0.0.1";
example = "0.0.0.0";
description = "Prefect server host";
};
port = lib.mkOption {
type = port;
default = 4200;
description = "Prefect server port";
};
dataDir = lib.mkOption {
type = path;
default = "/var/lib/prefect-server";
description = ''
Specify the directory for Prefect.
'';
};
database = lib.mkOption {
type = enum [
"sqlite"
"postgres"
];
default = "sqlite";
description = "which database to use for prefect server: sqlite or postgres";
};
databaseHost = lib.mkOption {
type = str;
default = "localhost";
description = "database host for postgres only";
};
databasePort = lib.mkOption {
type = str;
default = "5432";
description = "database port for postgres only";
};
databaseName = lib.mkOption {
type = str;
default = "prefect";
description = "database name for postgres only";
};
databaseUser = lib.mkOption {
type = str;
default = "postgres";
description = "database user for postgres only";
};
databasePasswordFile = lib.mkOption {
type = nullOr str;
default = null;
description = ''
path to a file containing e.g.:
DBPASSWORD=supersecret
stored outside the nix store, read by systemd as EnvironmentFile.
'';
};
# now define workerPools as an attribute set of submodules,
# each key is the pool name, and the submodule has an installPolicy
workerPools = lib.mkOption {
type = attrsOf (submodule {
options = {
installPolicy = lib.mkOption {
type = enum [
"always"
"if-not-present"
"never"
"prompt"
];
default = "always";
description = "install policy for the worker (always, if-not-present, never, prompt)";
};
};
});
default = { };
description = ''
define a set of worker pools with submodule config. example:
workerPools.my-pool = {
installPolicy = "never";
};
'';
};
baseUrl = lib.mkOption {
type = nullOr str;
default = null;
description = "external url when served by a reverse proxy, e.g. https://example.com/prefect";
};
};
config = lib.mkIf cfg.enable {
# define systemd.services as the server plus any worker definitions
systemd.services =
{
"prefect-server" = {
description = "prefect server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
DynamicUser = true;
StateDirectory = "prefect-server";
# TODO all my efforts to setup the database url
# have failed with some unable to open file
Environment = [
"PREFECT_HOME=%S/prefect-server"
"PREFECT_UI_STATIC_DIRECTORY=%S/prefect-server"
"PREFECT_SERVER_ANALYTICS_ENABLED=off"
"PREFECT_UI_API_URL=${cfg.baseUrl}/api"
"PREFECT_UI_URL=${cfg.baseUrl}"
];
EnvironmentFile =
if cfg.database == "postgres" && cfg.databasePasswordFile != null then
[ cfg.databasePasswordFile ]
else
[ ];
# ReadWritePaths = [ cfg.dataDir ];
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
NoNewPrivileges = true;
MemoryDenyWriteExecute = true;
LockPersonality = true;
CapabilityBoundingSet = [ ];
AmbientCapabilities = [ ];
RestrictSUIDSGID = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
MemoryAccounting = true;
CPUAccounting = true;
ExecStart = "${pkgs.prefect}/bin/prefect server start --host ${cfg.host} --port ${toString cfg.port}";
Restart = "always";
WorkingDirectory = cfg.dataDir;
};
};
}
// lib.concatMapAttrs (poolName: poolCfg: {
# return a partial attr set with one key: "prefect-worker-..."
"prefect-worker-${poolName}" = {
description = "prefect worker for pool '${poolName}'";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
environment.systemPackages = cfg.package;
serviceConfig = {
DynamicUser = true;
StateDirectory = "prefect-worker-${poolName}";
Environment = [
"PREFECT_HOME=%S/prefect-worker-${poolName}"
"PREFECT_API_URL=${cfg.baseUrl}/api"
];
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
NoNewPrivileges = true;
MemoryDenyWriteExecute = true;
LockPersonality = true;
CapabilityBoundingSet = [ ];
AmbientCapabilities = [ ];
RestrictSUIDSGID = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
MemoryAccounting = true;
CPUAccounting = true;
ExecStart = ''
${pkgs.prefect}/bin/prefect worker start \
--pool ${poolName} \
--type process \
--install-policy ${poolCfg.installPolicy}
'';
Restart = "always";
};
};
}) cfg.workerPools;
};
}
@@ -51,6 +51,7 @@ let
cfg.provision.idmAdminPasswordFile
cfg.provision.adminPasswordFile
]
++ optional (cfg.provision.extraJsonFile != null) cfg.provision.extraJsonFile
++ mapAttrsToList (_: x: x.basicSecretFile) cfg.provision.systems.oauth2
);
secretDirectories = unique (
@@ -9,6 +9,7 @@
options.services.paretosecurity = {
enable = lib.mkEnableOption "[ParetoSecurity](https://paretosecurity.com) [agent](https://github.com/ParetoSecurity/agent) and its root helper";
package = lib.mkPackageOption pkgs "paretosecurity" { };
trayIcon = lib.mkEnableOption "tray icon for ParetoSecurity";
};
config = lib.mkIf config.services.paretosecurity.enable {
@@ -39,5 +40,12 @@
};
};
systemd.user.services."paretosecurity-trayicon" = lib.mkIf config.services.paretosecurity.trayIcon {
wantedBy = [ "graphical-session.target" ];
serviceConfig = {
ExecStart = "${config.services.paretosecurity.package}/bin/paretosecurity trayicon";
};
};
};
}
+30 -2
View File
@@ -128,10 +128,29 @@ let
siteOpts = { options, config, lib, name, ... }:
{
# TODO: Remove in time for 25.11 and/or simplify once https://github.com/NixOS/nixpkgs/issues/96006 is fixed
imports = [
({config, options, ... }: let
removalNote = "The option has had no effect for 3+ years. There is no replacement available.";
optPath = lib.options.showOption [ "services" "dokuwiki" "sites" name "enable" ];
in {
options.enable = mkOption {
visible = false;
apply = x: throw "The option `${optPath}' can no longer be used since it's been removed. ${removalNote}";
};
config.assertions = [
{
assertion = !options.enable.isDefined;
message = ''
The option definition `${optPath}' in ${showFiles options.enable.files} no longer has any effect; please remove it.
${removalNote}
'';
}
];
})
];
options = {
enable = mkEnableOption "DokuWiki web application";
package = mkPackageOption pkgs "dokuwiki" { };
stateDir = mkOption {
@@ -342,6 +361,13 @@ let
'';
};
# TODO: Remove when no submodule-level assertions are needed anymore
assertions = mkOption {
type = types.listOf types.unspecified;
default = [ ];
visible = false;
internal = true;
};
};
};
in
@@ -374,6 +400,8 @@ in
# implementation
config = mkIf (eachSite != {}) (mkMerge [{
# TODO: Remove when no submodule-level assertions are needed anymore
assertions = flatten (mapAttrsToList (_: cfg: cfg.assertions) eachSite);
services.phpfpm.pools = mapAttrs' (hostName: cfg: (
nameValuePair "dokuwiki-${hostName}" {
+13 -6
View File
@@ -75,9 +75,12 @@ in
);
# Hardening
AmbientCapabilities = "";
CapabilityBoundingSet = [ "" ];
DeviceAllow = [ "" ];
DevicePolicy = "closed";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = true;
@@ -91,15 +94,19 @@ in
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RemoveIPC = true;
RestrictAddressFamilies = [ "AF_INET AF_INET6 AF_UNIX" ];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
"~@resources"
"setrlimit"
];
UMask = "0077";
};
};
+3 -15
View File
@@ -102,24 +102,12 @@ in
phpPackage = cfg.phpPackage.buildEnv {
extensions =
{ all, enabled }:
with all;
[
enabled
++ (with all; [
apcu
ctype
curl
dom
exif
filter
gd
mbstring
opcache
openssl
session
simplexml
xml
yaml
zip
];
]);
extraConfig = generators.toKeyValue { mkKeyValue = generators.mkKeyValueDefault { } " = "; } {
output_buffering = "0";
+39 -1
View File
@@ -90,6 +90,7 @@ in
enum
package
port
listOf
;
assertStringPath =
@@ -288,6 +289,25 @@ in
'';
};
realmFiles = mkOption {
type = listOf path;
example = lib.literalExpression ''
[
./some/realm.json
./another/realm.json
]
'';
default = [ ];
description = ''
Realm files that the server is going to import during startup.
If a realm already exists in the server, the import operation is
skipped. Importing the master realm is not supported. All files are
expected to be in `json` format. See the
[documentation](https://www.keycloak.org/server/importExport) for
further information.
'';
};
settings = mkOption {
type = lib.types.submodule {
freeformType = attrsOf (
@@ -644,6 +664,24 @@ in
'';
};
systemd.tmpfiles.settings."10-keycloak" =
let
mkTarget =
file:
let
baseName = builtins.baseNameOf file;
name = if lib.hasSuffix ".json" baseName then baseName else "${baseName}.json";
in
"/run/keycloak/data/import/${name}";
settingsList = map (f: {
name = mkTarget f;
value = {
"L+".argument = "${f}";
};
}) cfg.realmFiles;
in
builtins.listToAttrs settingsList;
systemd.services.keycloak =
let
databaseServices =
@@ -725,7 +763,7 @@ in
cp $CREDENTIALS_DIRECTORY/ssl_{cert,key} /run/keycloak/ssl/
''
+ ''
kc.sh --verbose start --optimized
kc.sh --verbose start --optimized ${lib.optionalString (cfg.realmFiles != [ ]) "--import-realm"}
'';
};
+9 -1
View File
@@ -320,9 +320,17 @@ in
umask $oldUmask
# Ensure that our local.yaml is valid (see kimai:reload command).
${pkg hostName cfg}/bin/console lint:yaml --parse-tags \
${pkg hostName cfg}/share/php/kimai/config
# Run kimai:install to ensure database is created or updated.
# Note that kimai:update is an alias to kimai:install.
${pkg hostName cfg}/bin/console kimai:install
${pkg hostName cfg}/bin/console kimai:install --no-cache
# Clear cache and warmup cache separately, to avoid "Cannot declare
# class App\Entity\Timesheet" error on first init after upgrade.
${pkg hostName cfg}/bin/console cache:clear --env=prod
${pkg hostName cfg}/bin/console cache:warmup --env=prod
'';
serviceConfig = {
@@ -398,6 +398,7 @@ in
};
systemd.tmpfiles.rules = [
"d /var/lib/mobilizon/sitemap 700 mobilizon mobilizon - -"
"d /var/lib/mobilizon/uploads/exports/csv 700 mobilizon mobilizon - -"
"Z /var/lib/mobilizon 700 mobilizon mobilizon - -"
];
+8 -1
View File
@@ -159,18 +159,25 @@ in
Group = config.users.users.wakapi.group;
DynamicUser = true;
PrivateTmp = true;
PrivateUsers = true;
PrivateDevices = true;
ProtectHome = true;
ProtectHostname = true;
ProtectClock = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectControlGroups = true;
NoNewPrivileges = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
ProtectSystem = "full";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
+11 -6
View File
@@ -107,9 +107,11 @@ in
ExecStart = lib.getExe cfg.package;
# Hardening
AmbientCapabilities = "";
CapabilityBoundingSet = [ "" ];
DeviceAllow = [ "" ];
DevicePolicy = "closed";
LockPersonality = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = true;
@@ -123,15 +125,18 @@ in
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RemoveIPC = true;
RestrictAddressFamilies = [ "AF_INET AF_INET6 AF_UNIX AF_NETLINK" ];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
"~@resources"
];
UMask = "0077";
};
};
};
@@ -29,10 +29,12 @@ def config(*path: List[str]) -> Optional[Any]:
def get_system_path(profile: str = 'system', gen: Optional[str] = None, spec: Optional[str] = None) -> str:
basename = f'{profile}-{gen}-link' if gen is not None else profile
profiles_dir = '/nix/var/nix/profiles'
if profile == 'system':
result = os.path.join('/nix', 'var', 'nix', 'profiles', 'system')
result = os.path.join(profiles_dir, basename)
else:
result = os.path.join('/nix', 'var', 'nix', 'profiles', 'system-profiles', profile + f'-{gen}-link' if gen is not None else '')
result = os.path.join(profiles_dir, 'system-profiles', basename)
if spec is not None:
result = os.path.join(result, 'specialisation', spec)
@@ -169,8 +171,8 @@ def generate_config_entry(profile: str, gen: str) -> str:
boot_spec = bootjson_to_bootspec(boot_json)
entry = config_entry(2, boot_spec, f'Generation {gen}', time)
for spec in boot_spec.specialisations:
entry += config_entry(2, boot_spec, f'Generation {gen}, Specialisation {spec}', str(time))
for spec, spec_boot_spec in boot_spec.specialisations.items():
entry += config_entry(2, spec_boot_spec, f'Generation {gen}, Specialisation {spec}', str(time))
return entry
+9
View File
@@ -19,6 +19,7 @@ let
kernel = config.system.modulesTree;
firmware = config.hardware.firmware;
allowMissing = false;
inherit (config.boot.initrd) extraFirmwarePaths;
};
@@ -484,6 +485,14 @@ in
'';
};
boot.initrd.extraFirmwarePaths = mkOption {
default = [ ];
type = types.listOf types.str;
description = ''
Other firmware files (relative to `"''${config.hardware.firmware}/lib/firmware"`) to include in the final initrd we are building.
'';
};
boot.initrd.checkJournalingFS = mkOption {
default = true;
type = types.bool;
+4 -2
View File
@@ -104,7 +104,7 @@ let
"sleep.target"
"hybrid-sleep.target"
"systemd-hibernate.service"
"systemd-hibernate-clear.service"
] ++ (lib.optional cfg.package.withEfi "systemd-hibernate-clear.service") ++ [
"systemd-hybrid-sleep.service"
"systemd-suspend.service"
"systemd-suspend-then-hibernate.service"
@@ -130,11 +130,13 @@ let
"systemd-ask-password-wall.service"
# Varlink APIs
] ++ lib.optionals cfg.package.withBootloader [
"systemd-bootctl@.service"
"systemd-bootctl.socket"
] ++ [
"systemd-creds@.service"
"systemd-creds.socket"
] ++ lib.optional cfg.package.withTpm2Tss [
] ++ lib.optional cfg.package.withTpm2Units [
"systemd-pcrlock@.service"
"systemd-pcrlock.socket"
] ++ [
+6 -4
View File
@@ -105,6 +105,7 @@ let
kernel = config.system.modulesTree;
firmware = config.hardware.firmware;
allowMissing = false;
inherit (config.boot.initrd) extraFirmwarePaths;
};
initrdBinEnv = pkgs.buildEnv {
@@ -703,10 +704,11 @@ in
"|stage1panic"
];
};
script = ''
echo c > /proc/sysrq-trigger
'';
serviceConfig.Type = "oneshot";
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.coreutils}/bin/echo c";
StandardOutput = "file:/proc/sysrq-trigger";
};
};
};
};
@@ -11,7 +11,7 @@ let
cfg = config.systemd.sysusers;
userCfg = config.users;
systemUsers = lib.filterAttrs (_username: opts: !opts.isNormalUser) userCfg.users;
systemUsers = lib.filterAttrs (_username: opts: opts.enable && !opts.isNormalUser) userCfg.users;
sysusersConfig = pkgs.writeTextDir "00-nixos.conf" ''
# Type Name ID GECOS Home directory Shell
@@ -89,7 +89,7 @@ in
}
]
++ (lib.mapAttrsToList (username: opts: {
assertion = !opts.isNormalUser;
assertion = opts.enable -> !opts.isNormalUser;
message = "${username} is a normal user. systemd-sysusers doesn't create normal users, only system users.";
}) userCfg.users)
++ lib.mapAttrsToList (username: opts: {
+4 -4
View File
@@ -27,13 +27,13 @@
options = {
systemd.tpm2.enable = lib.mkEnableOption "systemd TPM2 support" // {
default = config.systemd.package.withTpm2Tss;
defaultText = "systemd.package.withTpm2Tss";
default = config.systemd.package.withTpm2Units;
defaultText = "systemd.package.withTpm2Units";
};
boot.initrd.systemd.tpm2.enable = lib.mkEnableOption "systemd initrd TPM2 support" // {
default = config.boot.initrd.systemd.package.withTpm2Tss;
defaultText = "boot.initrd.systemd.package.withTpm2Tss";
default = config.boot.initrd.systemd.package.withTpm2Units;
defaultText = "boot.initrd.systemd.package.withTpm2Units";
};
};
+12
View File
@@ -58,6 +58,18 @@ in
vmWithBootLoader = lib.mkDefault config.virtualisation.vmVariantWithBootLoader.system.build.vm;
};
virtualisation.vmVariant = {
options = {
virtualisation.vmVariant = lib.mkOption {
apply = _: throw "virtualisation.vmVariant*.virtualisation.vmVariant is not supported";
};
virtualisation.vmVariantWithBootLoader = lib.mkOption {
apply =
_: throw "virtualisation.vmVariant*.virtualisation.vmVariantWithBootloader is not supported";
};
};
};
};
# uses extendModules
+10 -6
View File
@@ -1,6 +1,6 @@
# Systemd services for docker.
{ config, lib, pkgs, ... }:
{ config, lib, utils, pkgs, ... }:
with lib;
@@ -271,11 +271,15 @@ in
restartIfChanged = false;
unitConfig.X-StopOnRemoval = false;
serviceConfig.Type = "oneshot";
script = ''
${cfg.package}/bin/docker system prune -f ${toString cfg.autoPrune.flags}
'';
serviceConfig = {
Type = "oneshot";
ExecStart = utils.escapeSystemdExecArgs ([
(lib.getExe cfg.package)
"system"
"prune"
"-f"
] ++ cfg.autoPrune.flags);
};
startAt = optional cfg.autoPrune.enable cfg.autoPrune.dates;
after = [ "docker.service" ];
+1
View File
@@ -306,6 +306,7 @@ in
};
boot.kernelModules = [
"br_netfilter"
"veth"
"xt_comment"
"xt_CHECKSUM"
@@ -178,7 +178,6 @@ let
exec ${config.systemd.package}/bin/systemd-nspawn \
--keep-unit \
-M "$INSTANCE" -D "$root" "''${extraFlags[@]}" \
$EXTRA_NSPAWN_FLAGS \
--notify-ready=yes \
--kill-signal=SIGRTMIN+3 \
--bind-ro=/nix/store:/nix/store$NIX_BIND_OPT \
@@ -203,6 +202,7 @@ let
${optionalString (cfg.tmpfs != null && cfg.tmpfs != [])
''--tmpfs=${concatStringsSep " --tmpfs=" cfg.tmpfs}''
} \
$EXTRA_NSPAWN_FLAGS \
${containerInit cfg} "''${SYSTEM_PATH:-/nix/var/nix/profiles/system}/init"
'';
@@ -1,4 +1,4 @@
{ config, lib, pkgs, ... }:
{ config, lib, utils, pkgs, ... }:
let
cfg = config.virtualisation.podman;
json = pkgs.formats.json { };
@@ -202,11 +202,15 @@ in
restartIfChanged = false;
unitConfig.X-StopOnRemoval = false;
serviceConfig.Type = "oneshot";
script = ''
${cfg.package}/bin/podman system prune -f ${toString cfg.autoPrune.flags}
'';
serviceConfig = {
Type = "oneshot";
ExecStart = utils.escapeSystemdExecArgs ([
(lib.getExe cfg.package)
"system"
"prune"
"-f"
] ++ cfg.autoPrune.flags);
};
startAt = lib.optional cfg.autoPrune.enable cfg.autoPrune.dates;
after = [ "podman.service" ];
-1
View File
@@ -85,7 +85,6 @@ rec {
(onFullSupported "nixos.tests.containers-imperative")
(onFullSupported "nixos.tests.containers-ip")
(onSystems [ "x86_64-linux" ] "nixos.tests.docker")
(onFullSupported "nixos.tests.ecryptfs")
(onFullSupported "nixos.tests.env")
# Way too many manual retries required on Hydra.
+10 -5
View File
@@ -264,7 +264,7 @@ in {
cinnamon = handleTest ./cinnamon.nix {};
cinnamon-wayland = handleTest ./cinnamon-wayland.nix {};
cjdns = handleTest ./cjdns.nix {};
clatd = handleTest ./clatd.nix {};
clatd = runTest ./clatd.nix;
clickhouse = handleTest ./clickhouse.nix {};
cloud-init = handleTest ./cloud-init.nix {};
cloud-init-hostname = handleTest ./cloud-init-hostname.nix {};
@@ -353,7 +353,7 @@ in {
early-mount-options = handleTest ./early-mount-options.nix {};
ec2-config = (handleTestOn ["x86_64-linux"] ./ec2.nix {}).boot-ec2-config or {};
ec2-nixops = (handleTestOn ["x86_64-linux"] ./ec2.nix {}).boot-ec2-nixops or {};
echoip = handleTest ./echoip.nix {};
echoip = runTest ./echoip.nix;
ecryptfs = handleTest ./ecryptfs.nix {};
fscrypt = handleTest ./fscrypt.nix {};
fastnetmon-advanced = runTest ./fastnetmon-advanced.nix;
@@ -425,6 +425,7 @@ in {
firewall = handleTest ./firewall.nix { nftables = false; };
firewall-nftables = handleTest ./firewall.nix { nftables = true; };
fish = runTest ./fish.nix;
firezone = handleTest ./firezone/firezone.nix {};
flannel = handleTestOn ["x86_64-linux"] ./flannel.nix {};
flaresolverr = handleTest ./flaresolverr.nix {};
flood = handleTest ./flood.nix {};
@@ -432,6 +433,7 @@ in {
imports = [ ./firefox.nix ] ;
_module.args.firefoxPackage = pkgs.floorp;
};
fluent-bit = handleTest ./fluent-bit.nix {};
fluentd = handleTest ./fluentd.nix {};
fluidd = handleTest ./fluidd.nix {};
fontconfig-default-fonts = handleTest ./fontconfig-default-fonts.nix {};
@@ -969,6 +971,7 @@ in {
pppd = handleTest ./pppd.nix {};
predictable-interface-names = handleTest ./predictable-interface-names.nix {};
pretalx = runTest ./web-apps/pretalx.nix;
prefect = runTest ./prefect.nix;
pretix = runTest ./web-apps/pretix.nix;
printing-socket = handleTest ./printing.nix { socket = true; listenTcp = true; };
printing-service = handleTest ./printing.nix { socket = false; listenTcp = true; };
@@ -996,7 +999,7 @@ in {
qgis = handleTest ./qgis.nix { package = pkgs.qgis; };
qgis-ltr = handleTest ./qgis.nix { package = pkgs.qgis-ltr; };
qownnotes = handleTest ./qownnotes.nix {};
qtile = handleTestOn ["x86_64-linux" "aarch64-linux"] ./qtile/default.nix {};
qtile = runTestOn ["x86_64-linux" "aarch64-linux"] ./qtile/default.nix;
quake3 = handleTest ./quake3.nix {};
quicktun = handleTest ./quicktun.nix {};
quickwit = handleTest ./quickwit.nix {};
@@ -1005,7 +1008,7 @@ in {
radarr = handleTest ./radarr.nix {};
radicale = handleTest ./radicale.nix {};
radicle = runTest ./radicle.nix;
ragnarwm = handleTest ./ragnarwm.nix {};
ragnarwm = runTestOn ["x86_64-linux" "aarch64-linux"] ./ragnarwm.nix;
rasdaemon = handleTest ./rasdaemon.nix {};
rathole = runTest ./rathole.nix;
readarr = handleTest ./readarr.nix {};
@@ -1049,7 +1052,7 @@ in {
scaphandre = handleTest ./scaphandre.nix {};
schleuder = handleTest ./schleuder.nix {};
scion-freestanding-deployment = handleTest ./scion/freestanding-deployment {};
scrutiny = handleTest ./scrutiny.nix {};
scrutiny = runTest ./scrutiny.nix;
sddm = handleTest ./sddm.nix {};
sdl3 = handleTest ./sdl3.nix { };
seafile = handleTest ./seafile.nix {};
@@ -1273,12 +1276,14 @@ in {
vault-dev = handleTest ./vault-dev.nix {};
vault-postgresql = handleTest ./vault-postgresql.nix {};
vaultwarden = discoverTests (import ./vaultwarden.nix);
vdirsyncer = handleTest ./vdirsyncer.nix {};
vector = handleTest ./vector {};
velocity = runTest ./velocity.nix;
vengi-tools = handleTest ./vengi-tools.nix {};
victoriametrics = handleTest ./victoriametrics {};
vikunja = handleTest ./vikunja.nix {};
virtualbox = handleTestOn ["x86_64-linux"] ./virtualbox.nix {};
vm-variant = handleTest ./vm-variant.nix {};
vscode-remote-ssh = handleTestOn ["x86_64-linux"] ./vscode-remote-ssh.nix {};
vscodium = discoverTests (import ./vscodium.nix);
vsftpd = handleTest ./vsftpd.nix {};
+166 -184
View File
@@ -26,160 +26,160 @@
# | Route: 192.0.2.0/24 via 100.64.0.1
# +------
import ./make-test-python.nix (
{ pkgs, lib, ... }:
{ lib, ... }:
{
name = "clatd";
meta = with pkgs.lib.maintainers; {
maintainers = [
hax404
jmbaur
{
name = "clatd";
meta.maintainers = with lib.maintainers; [
hax404
jmbaur
];
nodes = {
# The server is configured with static IPv4 addresses. RFC 6052 Section 3.1
# disallows the mapping of non-global IPv4 addresses like RFC 1918 into the
# Well-Known Prefix 64:ff9b::/96. TAYGA also does not allow the mapping of
# documentation space (RFC 5737). To circumvent this, 100.64.0.2/24 from
# RFC 6589 (Carrier Grade NAT) is used here.
# To reach the IPv4 address pool of the NAT64 gateway, there is a static
# route configured. In normal cases, where the router would also source NAT
# the pool addresses to one IPv4 addresses, this would not be needed.
server = {
virtualisation.vlans = [
2 # towards router
];
networking = {
useDHCP = false;
interfaces.eth1 = lib.mkForce { };
};
systemd.network = {
enable = true;
networks."vlan1" = {
matchConfig.Name = "eth1";
address = [
"100.64.0.2/24"
];
routes = [
{
Destination = "192.0.2.0/24";
Gateway = "100.64.0.1";
}
];
};
};
};
nodes = {
# The server is configured with static IPv4 addresses. RFC 6052 Section 3.1
# disallows the mapping of non-global IPv4 addresses like RFC 1918 into the
# Well-Known Prefix 64:ff9b::/96. TAYGA also does not allow the mapping of
# documentation space (RFC 5737). To circumvent this, 100.64.0.2/24 from
# RFC 6589 (Carrier Grade NAT) is used here.
# To reach the IPv4 address pool of the NAT64 gateway, there is a static
# route configured. In normal cases, where the router would also source NAT
# the pool addresses to one IPv4 addresses, this would not be needed.
server = {
virtualisation.vlans = [
2 # towards router
];
networking = {
useDHCP = false;
interfaces.eth1 = lib.mkForce { };
};
systemd.network = {
enable = true;
networks."vlan1" = {
matchConfig.Name = "eth1";
address = [
"100.64.0.2/24"
];
routes = [
# The router is configured with static IPv4 addresses towards the server
# and IPv6 addresses towards the client. DNS64 is exposed towards the
# client so clatd is able to auto-discover the PLAT prefix. For NAT64, the
# Well-Known prefix 64:ff9b::/96 is used. NAT64 is done with TAYGA which
# provides the tun-interface nat64 and does the translation over it. The
# IPv6 packets are sent to this interfaces and received as IPv4 packets and
# vice versa. As TAYGA only translates IPv6 addresses to dedicated IPv4
# addresses, it needs a pool of IPv4 addresses which must be at least as
# big as the expected amount of clients. In this test, the packets from the
# pool are directly routed towards the client. In normal cases, there would
# be a second source NAT44 to map all clients behind one IPv4 address.
router = {
boot.kernel.sysctl = {
"net.ipv4.conf.all.forwarding" = 1;
"net.ipv6.conf.all.forwarding" = 1;
};
virtualisation.vlans = [
2 # towards server
3 # towards client
];
networking = {
useDHCP = false;
useNetworkd = true;
firewall.enable = false;
interfaces.eth1 = lib.mkForce {
ipv4 = {
addresses = [
{
Destination = "192.0.2.0/24";
Gateway = "100.64.0.1";
address = "100.64.0.1";
prefixLength = 24;
}
];
};
};
interfaces.eth2 = lib.mkForce {
ipv6 = {
addresses = [
{
address = "2001:db8::1";
prefixLength = 64;
}
];
};
};
};
# The router is configured with static IPv4 addresses towards the server
# and IPv6 addresses towards the client. DNS64 is exposed towards the
# client so clatd is able to auto-discover the PLAT prefix. For NAT64, the
# Well-Known prefix 64:ff9b::/96 is used. NAT64 is done with TAYGA which
# provides the tun-interface nat64 and does the translation over it. The
# IPv6 packets are sent to this interfaces and received as IPv4 packets and
# vice versa. As TAYGA only translates IPv6 addresses to dedicated IPv4
# addresses, it needs a pool of IPv4 addresses which must be at least as
# big as the expected amount of clients. In this test, the packets from the
# pool are directly routed towards the client. In normal cases, there would
# be a second source NAT44 to map all clients behind one IPv4 address.
router = {
boot.kernel.sysctl = {
"net.ipv4.conf.all.forwarding" = 1;
"net.ipv6.conf.all.forwarding" = 1;
};
virtualisation.vlans = [
2 # towards server
3 # towards client
];
networking = {
useDHCP = false;
useNetworkd = true;
firewall.enable = false;
interfaces.eth1 = lib.mkForce {
ipv4 = {
addresses = [
{
address = "100.64.0.1";
prefixLength = 24;
}
];
};
};
interfaces.eth2 = lib.mkForce {
ipv6 = {
addresses = [
{
address = "2001:db8::1";
prefixLength = 64;
}
];
};
};
};
systemd.network.networks."40-eth2" = {
networkConfig.IPv6SendRA = true;
ipv6Prefixes = [ { Prefix = "2001:db8::/64"; } ];
ipv6PREF64Prefixes = [ { Prefix = "64:ff9b::/96"; } ];
ipv6SendRAConfig = {
EmitDNS = true;
DNS = "_link_local";
};
};
services.resolved.extraConfig = ''
DNSStubListener=no
'';
networking.extraHosts = ''
192.0.0.171 ipv4only.arpa
192.0.0.170 ipv4only.arpa
'';
services.coredns = {
enable = true;
config = ''
.:53 {
bind ::
hosts /etc/hosts
dns64 64:ff9b::/96
}
'';
};
services.tayga = {
enable = true;
ipv4 = {
address = "192.0.2.0";
router = {
address = "192.0.2.1";
};
pool = {
address = "192.0.2.0";
prefixLength = 24;
};
};
ipv6 = {
address = "2001:db8::1";
router = {
address = "64:ff9b::1";
};
pool = {
address = "64:ff9b::";
prefixLength = 96;
};
};
systemd.network.networks."40-eth2" = {
networkConfig.IPv6SendRA = true;
ipv6Prefixes = [ { Prefix = "2001:db8::/64"; } ];
ipv6PREF64Prefixes = [ { Prefix = "64:ff9b::/96"; } ];
ipv6SendRAConfig = {
EmitDNS = true;
DNS = "_link_local";
};
};
# The client uses SLAAC to assign IPv6 addresses. To reach the IPv4-only
# server, the client starts the clat daemon which starts and configures the
# local IPv4 -> IPv6 translation via Tayga after discovering the PLAT
# prefix via DNS64.
client = {
services.resolved.extraConfig = ''
DNSStubListener=no
'';
networking.extraHosts = ''
192.0.0.171 ipv4only.arpa
192.0.0.170 ipv4only.arpa
'';
services.coredns = {
enable = true;
config = ''
.:53 {
bind ::
hosts /etc/hosts
dns64 64:ff9b::/96
}
'';
};
services.tayga = {
enable = true;
ipv4 = {
address = "192.0.2.0";
router = {
address = "192.0.2.1";
};
pool = {
address = "192.0.2.0";
prefixLength = 24;
};
};
ipv6 = {
address = "2001:db8::1";
router = {
address = "64:ff9b::1";
};
pool = {
address = "64:ff9b::";
prefixLength = 96;
};
};
};
};
# The client uses SLAAC to assign IPv6 addresses. To reach the IPv4-only
# server, the client starts the clat daemon which starts and configures the
# local IPv4 -> IPv6 translation via Tayga after discovering the PLAT
# prefix via DNS64.
client =
{ pkgs, ... }:
{
virtualisation.vlans = [
3 # towards router
];
@@ -193,62 +193,44 @@ import ./make-test-python.nix (
enable = true;
networks."vlan1" = {
matchConfig.Name = "eth1";
# NOTE: clatd does not actually use the PREF64 prefix discovered by
# systemd-networkd (nor does systemd-networkd do anything with it,
# yet), but we set this to confirm it works. See the test script
# below.
ipv6AcceptRAConfig.UsePREF64 = true;
};
};
services.clatd = {
enable = true;
# NOTE: Perl's Net::DNS resolver does not seem to work well querying
# for AAAA records to systemd-resolved's default IPv4 bind address
# (127.0.0.53), so we add an IPv6 listener address to systemd-resolved
# and tell clatd to use that instead.
settings.dns64-servers = "::1";
};
# Allow clatd to find dns server. See comment above.
services.resolved.extraConfig = ''
DNSStubListenerExtra=::1
'';
services.clatd.enable = true;
environment.systemPackages = [ pkgs.mtr ];
};
};
};
testScript = ''
import json
testScript = ''
import json
start_all()
start_all()
# wait for all machines to start up
for machine in client, router, server:
machine.wait_for_unit("network-online.target")
# wait for all machines to start up
for machine in client, router, server:
machine.wait_for_unit("network.target")
with subtest("Wait for tayga and clatd"):
router.wait_for_unit("tayga.service")
client.wait_for_unit("clatd.service")
# clatd checks if this system has IPv4 connectivity for 10 seconds
client.wait_until_succeeds(
'journalctl -u clatd -e | grep -q "Starting up TAYGA, using config file"'
)
with subtest("Wait for tayga and clatd"):
router.wait_for_unit("tayga.service")
client.wait_for_unit("clatd.service")
# clatd checks if this system has IPv4 connectivity for 10 seconds
client.wait_until_succeeds(
'journalctl -u clatd -e | grep -q "Starting up TAYGA, using config file"'
)
with subtest("networkd exports PREF64 prefix"):
assert json.loads(client.succeed("networkctl status eth1 --json=short"))[
"NDisc"
]["PREF64"][0]["Prefix"] == [0x0, 0x64, 0xFF, 0x9B] + ([0] * 12)
with subtest("networkd exports PREF64 prefix"):
assert json.loads(client.succeed("networkctl status eth1 --json=short"))[
"NDisc"
]["PREF64"][0]["Prefix"] == [0x0, 0x64, 0xFF, 0x9B] + ([0] * 12)
with subtest("Test ICMP"):
client.wait_until_succeeds("ping -c 3 100.64.0.2 >&2")
with subtest("Test ICMP"):
client.wait_until_succeeds("ping -c3 100.64.0.2 >&2")
with subtest("Test ICMP and show a traceroute"):
client.wait_until_succeeds("mtr --show-ips --report-wide 100.64.0.2 >&2")
with subtest("Test ICMP and show a traceroute"):
client.wait_until_succeeds("mtr --show-ips --report-wide 100.64.0.2 >&2")
client.log(client.execute("systemd-analyze security clatd.service")[1])
'';
}
)
client.log(client.execute("systemd-analyze security clatd.service")[1])
'';
}
+23 -24
View File
@@ -1,29 +1,28 @@
import ./make-test-python.nix (
{ lib, ... }:
{
name = "echoip";
meta.maintainers = with lib.maintainers; [ defelo ];
{ lib, ... }:
nodes.machine = {
services.echoip = {
enable = true;
virtualHost = "echoip.local";
};
{
name = "echoip";
meta.maintainers = with lib.maintainers; [ defelo ];
networking.hosts = {
"127.0.0.1" = [ "echoip.local" ];
"::1" = [ "echoip.local" ];
};
nodes.machine = {
services.echoip = {
enable = true;
virtualHost = "echoip.local";
};
testScript = ''
machine.wait_for_unit("echoip.service")
machine.wait_for_open_port(8080)
networking.hosts = {
"127.0.0.1" = [ "echoip.local" ];
"::1" = [ "echoip.local" ];
};
};
resp = machine.succeed("curl -4 http://echoip.local/ip")
assert resp.strip() == "127.0.0.1"
resp = machine.succeed("curl -6 http://echoip.local/ip")
assert resp.strip() == "::1"
'';
}
)
testScript = ''
machine.wait_for_unit("echoip.service")
machine.wait_for_open_port(8080)
resp = machine.succeed("curl -4 http://echoip.local/ip")
assert resp.strip() == "127.0.0.1"
resp = machine.succeed("curl -6 http://echoip.local/ip")
assert resp.strip() == "::1"
'';
}
+86
View File
@@ -0,0 +1,86 @@
alias Domain.{Repo, Accounts, Auth, Actors, Tokens}
mappings = case File.read("provision-uuids.json") do
{:ok, content} ->
case Jason.decode(content) do
{:ok, mapping} -> mapping
_ -> %{"accounts" => %{}}
end
_ -> %{"accounts" => %{}}
end
IO.puts("INFO: Fetching account")
{:ok, account} = Accounts.fetch_account_by_id_or_slug("main")
IO.puts("INFO: Fetching email provider")
{:ok, email_provider} = Auth.Provider.Query.not_disabled()
|> Auth.Provider.Query.by_adapter(:email)
|> Auth.Provider.Query.by_account_id(account.id)
|> Repo.fetch(Auth.Provider.Query, [])
temp_admin_actor_email = "firezone-provision@localhost.local"
temp_admin_actor_context = %Auth.Context{
type: :browser,
user_agent: "Unspecified/0.0",
remote_ip: {127, 0, 0, 1},
remote_ip_location_region: "N/A",
remote_ip_location_city: "N/A",
remote_ip_location_lat: 0.0,
remote_ip_location_lon: 0.0
}
{:ok, temp_admin_actor} =
Actors.create_actor(account, %{
type: :account_admin_user,
name: "Token Provisioning"
})
{:ok, temp_admin_actor_email_identity} =
Auth.create_identity(temp_admin_actor, email_provider, %{
provider_identifier: temp_admin_actor_email,
provider_identifier_confirmation: temp_admin_actor_email
})
{:ok, temp_admin_actor_token} =
Auth.create_token(temp_admin_actor_email_identity, temp_admin_actor_context, "temporarynonce", DateTime.utc_now() |> DateTime.add(1, :hour))
{:ok, temp_admin_subject} =
Auth.build_subject(temp_admin_actor_token, temp_admin_actor_context)
{:ok, relay_group_token} =
Tokens.create_token(%{
"type" => :relay_group,
"expires_at" => DateTime.utc_now() |> DateTime.add(1, :hour),
"secret_fragment" => Domain.Crypto.random_token(32, encoder: :hex32),
"relay_group_id" => get_in(mappings, ["accounts", "main", "relay_groups", "my-relays"])
})
relay_group_encoded_token = Tokens.encode_fragment!(relay_group_token)
IO.puts("Created relay token: #{relay_group_encoded_token}")
File.write("relay_token.txt", relay_group_encoded_token)
{:ok, gateway_group_token} =
Tokens.create_token(%{
"type" => :gateway_group,
"expires_at" => DateTime.utc_now() |> DateTime.add(1, :hour),
"secret_fragment" => Domain.Crypto.random_token(32, encoder: :hex32),
"account_id" => get_in(mappings, ["accounts", "main", "id"]),
"gateway_group_id" => get_in(mappings, ["accounts", "main", "gateway_groups", "site"])
}, temp_admin_subject)
gateway_group_encoded_token = Tokens.encode_fragment!(gateway_group_token)
IO.puts("Created gateway group token: #{gateway_group_encoded_token}")
File.write("gateway_token.txt", gateway_group_encoded_token)
{:ok, service_account_actor_token} =
Tokens.create_token(%{
"type" => :client,
"expires_at" => DateTime.utc_now() |> DateTime.add(1, :hour),
"secret_fragment" => Domain.Crypto.random_token(32, encoder: :hex32),
"account_id" => get_in(mappings, ["accounts", "main", "id"]),
"actor_id" => get_in(mappings, ["accounts", "main", "actors", "client"])
})
service_account_actor_encoded_token = Tokens.encode_fragment!(service_account_actor_token)
IO.puts("Created service actor token: #{service_account_actor_encoded_token}")
File.write("client_token.txt", service_account_actor_encoded_token)
+349
View File
@@ -0,0 +1,349 @@
import ../make-test-python.nix (
{ pkgs, ... }:
let
certs = import ../common/acme/server/snakeoil-certs.nix;
domain = certs.domain;
in
{
name = "firezone";
meta.maintainers = with pkgs.lib.maintainers; [ oddlama ];
nodes = {
server =
{
config,
lib,
pkgs,
...
}:
{
security.pki.certificateFiles = [ certs.ca.cert ];
networking.extraHosts = ''
${config.networking.primaryIPAddress} ${domain}
${config.networking.primaryIPv6Address} ${domain}
'';
networking.firewall.allowedTCPPorts = [
80
443
];
services.nginx = {
enable = true;
virtualHosts.${domain} = {
sslCertificate = certs.${domain}.cert;
sslCertificateKey = certs.${domain}.key;
};
};
services.firezone.server = {
enable = true;
enableLocalDB = true;
nginx.enable = true;
# Doesn't need to work for this test, but needs to be configured
# otherwise the server will not start.
smtp = {
from = "firezone@example.com";
host = "mail.localhost";
port = 465;
implicitTls = true;
username = "firezone@example.com";
passwordFile = pkgs.writeText "tmpmailpasswd" "supermailpassword";
};
provision = {
enable = true;
accounts.main = {
name = "My Account";
relayGroups.my-relays.name = "Relays";
gatewayGroups.site.name = "Site";
actors = {
admin = {
type = "account_admin_user";
name = "Admin";
email = "admin@example.com";
};
client = {
type = "service_account";
name = "A client";
email = "client@example.com";
};
};
resources.res1 = {
type = "dns";
name = "Dns Resource";
address = "resource.example.com";
gatewayGroups = [ "site" ];
filters = [
{ protocol = "icmp"; }
{
protocol = "tcp";
ports = [ 80 ];
}
];
};
resources.res2 = {
type = "ip";
name = "Ip Resource";
address = "172.20.2.1";
gatewayGroups = [ "site" ];
};
resources.res3 = {
type = "cidr";
name = "Cidr Resource";
address = "172.20.1.0/24";
gatewayGroups = [ "site" ];
};
policies.pol1 = {
description = "Allow anyone res1 access";
group = "everyone";
resource = "res1";
};
policies.pol2 = {
description = "Allow anyone res2 access";
group = "everyone";
resource = "res2";
};
policies.pol3 = {
description = "Allow anyone res3 access";
group = "everyone";
resource = "res3";
};
};
};
api.externalUrl = "https://${domain}/api/";
web.externalUrl = "https://${domain}/";
};
systemd.services.firezone-server-domain.postStart = lib.mkAfter ''
${lib.getExe config.services.firezone.server.domain.package} rpc 'Code.eval_file("${./create-tokens.exs}")'
'';
};
relay =
{
nodes,
config,
lib,
...
}:
{
security.pki.certificateFiles = [ certs.ca.cert ];
networking.extraHosts = ''
${nodes.server.networking.primaryIPAddress} ${domain}
${nodes.server.networking.primaryIPv6Address} ${domain}
'';
services.firezone.relay = {
enable = true;
logLevel = "debug";
name = "test-relay";
apiUrl = "wss://${domain}/api/";
tokenFile = "/tmp/shared/relay_token.txt";
publicIpv4 = config.networking.primaryIPAddress;
publicIpv6 = config.networking.primaryIPv6Address;
openFirewall = true;
};
# Don't auto-start so we can wait until the token was provisioned
systemd.services.firezone-relay.wantedBy = lib.mkForce [ ];
};
# A resource that is only connected to the gateway,
# allowing us to confirm the VPN works
resource = {
virtualisation.vlans = [
1
2
];
networking.interfaces.eth1.ipv4.addresses = [
{
address = "172.20.1.1";
prefixLength = 24;
}
];
networking.interfaces.eth2.ipv4.addresses = [
{
address = "172.20.2.1";
prefixLength = 24;
}
];
networking.firewall.allowedTCPPorts = [
80
];
services.nginx = {
enable = true;
virtualHosts = {
"localhost" = {
default = true;
locations."/".extraConfig = ''
return 200 'greetings from the resource';
add_header Content-Type text/plain;
'';
};
};
};
};
gateway =
{
nodes,
lib,
...
}:
{
virtualisation.vlans = [
1
2
];
networking = {
interfaces.eth1.ipv4.addresses = [
{
address = "172.20.1.2";
prefixLength = 24;
}
];
interfaces.eth2.ipv4.addresses = [
{
address = "172.20.2.2";
prefixLength = 24;
}
];
firewall.enable = false;
nftables.enable = true;
nftables.tables."filter".family = "inet";
nftables.tables."filter".content = ''
chain incoming {
type filter hook input priority 0; policy accept;
}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
meta protocol ip iifname "tun-firezone" oifname { "eth1", "eth2" } masquerade random
}
chain forward {
type filter hook forward priority 0; policy drop;
iifname "tun-firezone" accept
oifname "tun-firezone" accept
}
chain output {
type filter hook output priority 0; policy accept;
}
'';
};
boot.kernel.sysctl."net.ipv4.ip_forward" = "1";
# boot.kernel.sysctl."net.ipv4.conf.all.src_valid_mark" = "1";
boot.kernel.sysctl."net.ipv6.conf.default.forwarding" = "1";
boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = "1";
security.pki.certificateFiles = [ certs.ca.cert ];
networking.extraHosts = ''
${nodes.server.networking.primaryIPAddress} ${domain}
${nodes.server.networking.primaryIPv6Address} ${domain}
172.20.1.1 resource.example.com
'';
services.firezone.gateway = {
enable = true;
logLevel = "debug";
name = "test-gateway";
apiUrl = "wss://${domain}/api/";
tokenFile = "/tmp/shared/gateway_token.txt";
};
# Don't auto-start so we can wait until the token was provisioned
systemd.services.firezone-gateway.wantedBy = lib.mkForce [ ];
};
client =
{
nodes,
lib,
...
}:
{
security.pki.certificateFiles = [ certs.ca.cert ];
networking.useNetworkd = true;
networking.extraHosts = ''
${nodes.server.networking.primaryIPAddress} ${domain}
${nodes.server.networking.primaryIPv6Address} ${domain}
'';
services.firezone.headless-client = {
enable = true;
logLevel = "debug";
name = "test-client-somebody";
apiUrl = "wss://${domain}/api/";
tokenFile = "/tmp/shared/client_token.txt";
};
# Don't auto-start so we can wait until the token was provisioned
systemd.services.firezone-headless-client.wantedBy = lib.mkForce [ ];
};
};
testScript =
{ ... }:
''
start_all()
with subtest("Start server"):
server.wait_for_unit("firezone.target")
server.wait_until_succeeds("curl -Lsf https://${domain} | grep 'Welcome to Firezone'")
server.wait_until_succeeds("curl -Ls https://${domain}/api | grep 'Not Found'")
# Wait for tokens and copy them to shared folder
server.wait_for_file("/var/lib/private/firezone/relay_token.txt")
server.wait_for_file("/var/lib/private/firezone/gateway_token.txt")
server.wait_for_file("/var/lib/private/firezone/client_token.txt")
server.succeed("cp /var/lib/private/firezone/*_token.txt /tmp/shared")
with subtest("Connect relay"):
relay.succeed("systemctl start firezone-relay")
relay.wait_for_unit("firezone-relay.service")
relay.wait_until_succeeds("journalctl --since -2m --unit firezone-relay.service --grep 'Connected to portal.*${domain}'", timeout=30)
with subtest("Connect gateway"):
gateway.succeed("systemctl start firezone-gateway")
gateway.wait_for_unit("firezone-gateway.service")
gateway.wait_until_succeeds("journalctl --since -2m --unit firezone-gateway.service --grep 'Connected to portal.*${domain}'", timeout=30)
relay.wait_until_succeeds("journalctl --since -2m --unit firezone-relay.service --grep 'Created allocation.*IPv4'", timeout=30)
relay.wait_until_succeeds("journalctl --since -2m --unit firezone-relay.service --grep 'Created allocation.*IPv6'", timeout=30)
# Assert both relay ips are known
gateway.wait_until_succeeds("journalctl --since -2m --unit firezone-gateway.service --grep 'Updated allocation.*relay_ip4.*Some.*relay_ip6.*Some'", timeout=30)
with subtest("Connect headless-client"):
client.succeed("systemctl start firezone-headless-client")
client.wait_for_unit("firezone-headless-client.service")
client.wait_until_succeeds("journalctl --since -2m --unit firezone-headless-client.service --grep 'Connected to portal.*${domain}'", timeout=30)
client.wait_until_succeeds("journalctl --since -2m --unit firezone-headless-client.service --grep 'Tunnel ready'", timeout=30)
with subtest("Check DNS based access"):
# Check that we can access the resource through the VPN via DNS
client.wait_until_succeeds("curl -4 -Lsf http://resource.example.com | grep 'greetings from the resource'")
client.wait_until_succeeds("curl -6 -Lsf http://resource.example.com | grep 'greetings from the resource'")
with subtest("Check CIDR based access"):
# Check that we can access the resource through the VPN via CIDR
client.wait_until_succeeds("ping -c1 -W1 172.20.1.1")
with subtest("Check IP based access"):
# Check that we can access the resource through the VPN via IP
client.wait_until_succeeds("ping -c1 -W1 172.20.2.1")
'';
}
)
+40
View File
@@ -0,0 +1,40 @@
import ./make-test-python.nix (
{ lib, pkgs, ... }:
{
name = "fluent-bit";
nodes.machine =
{ config, pkgs, ... }:
{
services.fluent-bit = {
enable = true;
settings = {
pipeline = {
inputs = [
{
name = "systemd";
systemd_filter = "_SYSTEMD_UNIT=fluent-bit.service";
}
];
outputs = [
{
name = "file";
path = "/var/log/fluent-bit";
file = "fluent-bit.out";
}
];
};
};
};
systemd.services.fluent-bit.serviceConfig.LogsDirectory = "fluent-bit";
};
testScript = ''
start_all()
machine.wait_for_unit("fluent-bit.service")
machine.wait_for_file("/var/log/fluent-bit/fluent-bit.out")
'';
}
)
+1 -1
View File
@@ -40,7 +40,7 @@
machine_custom_port.wait_for_open_port(5678)
soup = BeautifulSoup(machine_default.succeed("curl http://localhost:8080"))
expected_version = "${config.nodes.machine_default.services.glance.package.version}"
expected_version = "v${config.nodes.machine_default.services.glance.package.version}"
assert any(a.text == expected_version for a in soup.select(".footer a"))
'';
+3
View File
@@ -25,7 +25,10 @@ import ./make-test-python.nix (
machine.wait_for_open_port(80)
machine.wait_for_unit("multi-user.target")
# This establishes _something_
machine.succeed("curl -sSf http://localhost")
# The second request creates the database, unsure why both are required
machine.succeed("curl -sSf http://localhost/")
machine.succeed(
"curl -c cookies -sSf -X POST http://localhost/login -d 'username=admin&password=admin'"
+11 -32
View File
@@ -4,46 +4,25 @@ import ./make-test-python.nix (
name = "homepage-dashboard";
meta.maintainers = with lib.maintainers; [ jnsgruk ];
nodes.unmanaged_conf =
{ pkgs, ... }:
{
services.homepage-dashboard.enable = true;
};
nodes.managed_conf =
{ pkgs, ... }:
{
services.homepage-dashboard = {
enable = true;
settings.title = "test title rodUsEagid"; # something random/unique
};
nodes.machine = _: {
services.homepage-dashboard = {
enable = true;
settings.title = "test title rodUsEagid"; # something random/unique
};
};
testScript = ''
# Ensure the services are started on unmanaged machine
unmanaged_conf.wait_for_unit("homepage-dashboard.service")
unmanaged_conf.wait_for_open_port(8082)
unmanaged_conf.succeed("curl --fail http://localhost:8082/")
# Ensure that /etc/homepage-dashboard doesn't exist, and boilerplate
# configs are copied into place.
unmanaged_conf.fail("test -d /etc/homepage-dashboard")
unmanaged_conf.succeed("test -f /var/lib/private/homepage-dashboard/settings.yaml")
# Ensure the services are started on managed machine
managed_conf.wait_for_unit("homepage-dashboard.service")
managed_conf.wait_for_open_port(8082)
managed_conf.succeed("curl --fail http://localhost:8082/")
machine.wait_for_unit("homepage-dashboard.service")
machine.wait_for_open_port(8082)
machine.succeed("curl --fail http://localhost:8082/")
# Ensure /etc/homepage-dashboard is created and unmanaged conf location isn't.
managed_conf.succeed("test -d /etc/homepage-dashboard")
managed_conf.fail("test -f /var/lib/private/homepage-dashboard/settings.yaml")
# Ensure /etc/homepage-dashboard is created.
machine.succeed("test -d /etc/homepage-dashboard")
# Ensure that we see the custom title *only in the managed config*
page = managed_conf.succeed("curl --fail http://localhost:8082/")
page = machine.succeed("curl --fail http://localhost:8082/")
assert "test title rodUsEagid" in page, "Custom title not found"
page = unmanaged_conf.succeed("curl --fail http://localhost:8082/")
assert "test title rodUsEagid" not in page, "Custom title found where it shouldn't be"
'';
}
)
+4
View File
@@ -306,6 +306,10 @@ import ./make-test-python.nix (
provision.succeed('${specialisations}/credentialProvision/bin/switch-to-configuration test')
provision_login("${provisionIdmAdminPassword}")
# Make sure neither password is logged
provision.fail("journalctl --since -10m --unit kanidm.service --grep '${provisionAdminPassword}'")
provision.fail("journalctl --since -10m --unit kanidm.service --grep '${provisionIdmAdminPassword}'")
# Test provisioned admin pw
out = provision.succeed("KANIDM_PASSWORD=${provisionAdminPassword} kanidm login -D admin")
assert_contains(out, "Login Success for admin")
+47 -3
View File
@@ -3,14 +3,58 @@
name = "paretosecurity";
meta.maintainers = [ lib.maintainers.zupo ];
nodes.machine =
nodes.terminal =
{ config, pkgs, ... }:
{
imports = [ ./common/user-account.nix ];
services.paretosecurity.enable = true;
};
# very basic test for now, need to add output asserts
nodes.xfce =
{ config, pkgs, ... }:
{
imports = [ ./common/user-account.nix ];
services.paretosecurity = {
enable = true;
trayIcon = true;
};
services.xserver.enable = true;
services.xserver.displayManager.lightdm.enable = true;
services.xserver.desktopManager.xfce.enable = true;
services.displayManager.autoLogin = {
enable = true;
user = "alice";
};
environment.systemPackages = [ pkgs.xdotool ];
environment.variables.XAUTHORITY = "/home/alice/.Xauthority";
};
enableOCR = true;
testScript = ''
machine.wait_until_succeeds("paretosecurity check")
terminal.succeed(
"su -- alice -c 'paretosecurity check"
# Disable some checks that need intricate test setup so that this test
# remains simple and fast. Tests for all checks and edge cases available
# at https://github.com/ParetoSecurity/agent/tree/main/test/integration
+ " --skip c96524f2-850b-4bb9-abc7-517051b6c14e" # SecureBoot
+ " --skip 37dee029-605b-4aab-96b9-5438e5aa44d8" # Screen lock
+ " --skip 21830a4e-84f1-48fe-9c5b-beab436b2cdb" # Disk encryption
+ " --skip 44e4754a-0b42-4964-9cc2-b88b2023cb1e" # Pareto Security is up to date
+ " --skip f962c423-fdf5-428a-a57a-827abc9b253e" # Password manager installed
+ "'"
)
xfce.wait_for_x()
xfce.succeed("xdotool mousemove 850 10")
xfce.wait_for_text("Pareto Security")
xfce.succeed("xdotool click 1")
xfce.wait_for_text("Run Checks")
'';
}
+27
View File
@@ -0,0 +1,27 @@
{ lib, ... }:
let
mainPort = "4200";
in
{
name = "prefect";
nodes = {
machine =
{ ... }:
{
services.prefect = {
enable = true;
};
};
};
testScript = ''
machine.start()
machine.wait_for_unit("prefect-server.service")
machine.wait_for_open_port("${mainPort}")
'';
meta = with lib.maintainers; {
maintainers = [ happysalada ];
};
}
+44 -46
View File
@@ -1,54 +1,52 @@
import ../make-test-python.nix (
{ lib, ... }:
{
name = "qtile";
{ lib, ... }:
{
name = "qtile";
meta = {
maintainers = with lib.maintainers; [ sigmanificient ];
};
meta = {
maintainers = with lib.maintainers; [ sigmanificient ];
};
nodes.machine =
{ pkgs, lib, ... }:
let
# We create a custom Qtile configuration file that adds a widget from
# qtile-extras to the bar. This ensure that the qtile-extras package
# also works, and that extraPackages behave as expected.
nodes.machine =
{ pkgs, lib, ... }:
let
# We create a custom Qtile configuration file that adds a widget from
# qtile-extras to the bar. This ensure that the qtile-extras package
# also works, and that extraPackages behave as expected.
config-deriv = pkgs.callPackage ./config.nix { };
in
{
imports = [
../common/x11.nix
../common/user-account.nix
];
test-support.displayManager.auto.user = "alice";
config-deriv = pkgs.callPackage ./config.nix { };
in
{
imports = [
../common/x11.nix
../common/user-account.nix
];
test-support.displayManager.auto.user = "alice";
services.xserver.windowManager.qtile = {
enable = true;
configFile = "${config-deriv}/config.py";
extraPackages = ps: [ ps.qtile-extras ];
};
services.displayManager.defaultSession = lib.mkForce "qtile";
environment.systemPackages = [ pkgs.kitty ];
services.xserver.windowManager.qtile = {
enable = true;
configFile = "${config-deriv}/config.py";
extraPackages = ps: [ ps.qtile-extras ];
};
testScript = ''
with subtest("ensure x starts"):
machine.wait_for_x()
machine.wait_for_file("/home/alice/.Xauthority")
machine.succeed("xauth merge ~alice/.Xauthority")
services.displayManager.defaultSession = lib.mkForce "qtile";
with subtest("ensure client is available"):
machine.succeed("qtile --version")
environment.systemPackages = [ pkgs.kitty ];
};
with subtest("ensure we can open a new terminal"):
machine.sleep(2)
machine.send_key("meta_l-ret")
machine.wait_for_window(r"alice.*?machine")
machine.sleep(2)
machine.screenshot("terminal")
'';
}
)
testScript = ''
with subtest("ensure x starts"):
machine.wait_for_x()
machine.wait_for_file("/home/alice/.Xauthority")
machine.succeed("xauth merge ~alice/.Xauthority")
with subtest("ensure client is available"):
machine.succeed("qtile --version")
with subtest("ensure we can open a new terminal"):
machine.sleep(2)
machine.send_key("meta_l-ret")
machine.wait_for_window(r"alice.*?machine")
machine.sleep(2)
machine.screenshot("terminal")
'';
}
+34 -36
View File
@@ -1,40 +1,38 @@
import ./make-test-python.nix (
{ lib, ... }:
{
name = "ragnarwm";
{ lib, ... }:
{
name = "ragnarwm";
meta = {
maintainers = with lib.maintainers; [ sigmanificient ];
meta = {
maintainers = with lib.maintainers; [ sigmanificient ];
};
nodes.machine =
{ pkgs, lib, ... }:
{
imports = [
./common/x11.nix
./common/user-account.nix
];
test-support.displayManager.auto.user = "alice";
services.displayManager.defaultSession = lib.mkForce "ragnar";
services.xserver.windowManager.ragnarwm.enable = true;
# Setup the default terminal of Ragnar
environment.systemPackages = [ pkgs.alacritty ];
};
nodes.machine =
{ pkgs, lib, ... }:
{
imports = [
./common/x11.nix
./common/user-account.nix
];
test-support.displayManager.auto.user = "alice";
services.displayManager.defaultSession = lib.mkForce "ragnar";
services.xserver.windowManager.ragnarwm.enable = true;
testScript = ''
with subtest("ensure x starts"):
machine.wait_for_x()
machine.wait_for_file("/home/alice/.Xauthority")
machine.succeed("xauth merge ~alice/.Xauthority")
# Setup the default terminal of Ragnar
environment.systemPackages = [ pkgs.alacritty ];
};
testScript = ''
with subtest("ensure x starts"):
machine.wait_for_x()
machine.wait_for_file("/home/alice/.Xauthority")
machine.succeed("xauth merge ~alice/.Xauthority")
with subtest("ensure we can open a new terminal"):
# Sleeping a bit before the test, as it may help for sending keys
machine.sleep(2)
machine.send_key("meta_l-ret")
machine.wait_for_window(r"alice.*?machine")
machine.sleep(2)
machine.screenshot("terminal")
'';
}
)
with subtest("ensure we can open a new terminal"):
# Sleeping a bit before the test, as it may help for sending keys
machine.sleep(2)
machine.send_key("meta_l-ret")
machine.wait_for_window(r"alice.*?machine")
machine.sleep(2)
machine.screenshot("terminal")
'';
}
+90 -75
View File
@@ -1,83 +1,98 @@
import ./make-test-python.nix (
{ lib, ... }:
{ lib, ... }:
{
name = "scrutiny";
meta.maintainers = with lib.maintainers; [ jnsgruk ];
{
name = "scrutiny";
meta.maintainers = with lib.maintainers; [ jnsgruk ];
nodes = {
machine =
{
self,
pkgs,
lib,
...
}:
{
services = {
scrutiny.enable = true;
scrutiny.collector.enable = true;
nodes = {
machine =
{
self,
pkgs,
lib,
...
}:
{
services = {
scrutiny = {
enable = true;
settings = {
notify.urls = [
{
_secret = pkgs.writeText "notify-script" "script://${pkgs.writeShellScript "touch-test-file" ''
echo "HelloWorld" > /run/scrutiny/hello
''}";
}
];
};
};
environment.systemPackages =
let
seleniumScript =
pkgs.writers.writePython3Bin "selenium-script"
{
libraries = with pkgs.python3Packages; [ selenium ];
}
''
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
options = Options()
options.add_argument("--headless")
service = webdriver.FirefoxService(executable_path="${lib.getExe pkgs.geckodriver}") # noqa: E501
driver = webdriver.Firefox(options=options, service=service)
driver.implicitly_wait(10)
driver.get("http://localhost:8080/web/dashboard")
wait = WebDriverWait(driver, 10).until(
EC.text_to_be_present_in_element(
(By.TAG_NAME, "body"), "Drive health at a glance")
)
body_text = driver.find_element(By.TAG_NAME, "body").text
assert "Temperature history for each device" in body_text
driver.close()
'';
in
with pkgs;
[
curl
firefox-unwrapped
geckodriver
seleniumScript
];
scrutiny.collector.enable = true;
};
};
# This is the test code that will check if our service is running correctly:
testScript = ''
start_all()
# Wait for Scrutiny to be available
machine.wait_for_unit("scrutiny")
machine.wait_for_open_port(8080)
environment.systemPackages =
let
seleniumScript =
pkgs.writers.writePython3Bin "selenium-script"
{
libraries = with pkgs.python3Packages; [ selenium ];
}
''
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# Ensure the API responds as we expect
output = machine.succeed("curl localhost:8080/api/health")
assert output == '{"success":true}'
options = Options()
options.add_argument("--headless")
service = webdriver.FirefoxService(executable_path="${lib.getExe pkgs.geckodriver}") # noqa: E501
# Start the collector service to send some metrics
collect = machine.succeed("systemctl start scrutiny-collector.service")
driver = webdriver.Firefox(options=options, service=service)
driver.implicitly_wait(10)
driver.get("http://localhost:8080/web/dashboard")
# Ensure the application is actually rendered by the Javascript
machine.succeed("PYTHONUNBUFFERED=1 selenium-script")
'';
}
)
wait = WebDriverWait(driver, 10).until(
EC.text_to_be_present_in_element(
(By.TAG_NAME, "body"), "Drive health at a glance")
)
body_text = driver.find_element(By.TAG_NAME, "body").text
assert "Temperature history for each device" in body_text
driver.close()
'';
in
with pkgs;
[
curl
firefox-unwrapped
geckodriver
seleniumScript
];
};
};
# This is the test code that will check if our service is running correctly:
testScript = ''
start_all()
# Wait for Scrutiny to be available
machine.wait_for_unit("scrutiny")
machine.wait_for_open_port(8080)
# Ensure the API responds as we expect
output = machine.succeed("curl localhost:8080/api/health")
assert output == '{"success":true}'
# Start the collector service to send some metrics
collect = machine.succeed("systemctl start scrutiny-collector.service")
# Ensure the application is actually rendered by the Javascript
machine.succeed("PYTHONUNBUFFERED=1 selenium-script")
# Test notification and genJqSecretsReplacementSnippet
machine.succeed("curl -X POST http://localhost:8080/api/health/notify")
machine.wait_for_file("/run/scrutiny/hello")
output = machine.succeed("cat /run/scrutiny/hello")
assert "HelloWorld" in output
'';
}

Some files were not shown because too many files have changed in this diff Show More