Add a Nix flake with NixOS module and test

This commit is contained in:
Olivier 'reivilibre' 2024-05-21 21:42:08 +01:00
parent 8324adaef6
commit 10b6b350bd
8 changed files with 442 additions and 0 deletions

81
flake.lock generated Normal file
View File

@ -0,0 +1,81 @@
{
"nodes": {
"naersk": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1713520724,
"narHash": "sha256-CO8MmVDmqZX2FovL75pu5BvwhW+Vugc7Q6ze7Hj8heI=",
"owner": "nix-community",
"repo": "naersk",
"rev": "c5037590290c6c7dae2e42e7da1e247e54ed2d49",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1716218643,
"narHash": "sha256-i/E7gzQybvcGAYDRGDl39WL6yVk30Je/NXypBz6/nmM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a8695cbd09a7ecf3376bd62c798b9864d20f86ee",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-23.11",
"type": "indirect"
}
},
"root": {
"inputs": {
"naersk": "naersk",
"nixpkgs": "nixpkgs",
"utils": "utils"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

77
flake.nix Normal file
View File

@ -0,0 +1,77 @@
{
description = "idCoop: lightweight identity provider";
inputs = {
nixpkgs.url = "nixpkgs/nixos-23.11";
utils.url = "github:numtide/flake-utils";
naersk.url = "github:nix-community/naersk";
naersk.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, utils, naersk }:
utils.lib.eachDefaultSystem (system: let
pkgs = nixpkgs.legacyPackages."${system}";
lib = pkgs.lib;
naersk' = pkgs.callPackage naersk {
};
idcoop = naersk'.buildPackage {
pname = "idcoop";
# filter out .nix files from the input so they don't cause a recompilation when changed
src = lib.cleanSourceWith {
filter = path: type: !lib.hasSuffix ".nix" path;
src = ./.;
};
nativeBuildInputs = [
pkgs.makeWrapper
pkgs.pkg-config
pkgs.openssl
];
postInstall = ''
mkdir -p $out/share/idcoop
echo :::
ls
echo :::
cp -r static templates translations -t $out/share/idcoop/
wrapProgram $out/bin/idcoop --set HORNBEAM_BASE $out/share/idcoop
'';
};
in {
# `nix build`
packages = {
inherit idcoop;
default = idcoop;
};
# `nix run`
apps = rec {
idcoop = utils.lib.mkApp {
drv = idcoop;
};
default = idcoop;
};
}) // (let
forAllNixosSystems = nixpkgs.lib.genAttrs ["x86_64-linux" "aarch64-linux"];
in {
# NixOS Modules
nixosModules = {
idcoop = import ./nixos_module.nix self;
};
checks = forAllNixosSystems (system: let
checkArgs = {
pkgs = nixpkgs.legacyPackages.${system};
inherit self;
};
in {
starts = import ./nixos_tests/starts.nix checkArgs;
});
});
}

192
nixos_module.nix Normal file
View File

@ -0,0 +1,192 @@
flake: { config, pkgs, ... }:
let
inherit (flake.packages.${pkgs.stdenv.hostPlatform.system}) idcoop;
inherit (pkgs.lib) mkOption mkDefault mkIf types literalExpression mdDoc;
inherit (pkgs.writers) writeTOML;
inherit (builtins) mapAttrs;
defaultUser = "idcoop";
cfg = config.services.idcoop;
format = pkgs.formats.toml { };
oidcClientSubmodule = types.submodule {
# freeformType = format.type; — one day we may want to enable freeform types, but for now just keep it strongly defined
options = {
name = mkOption {
type = types.str;
description = ''
User-friendly name of the OIDC Client.
'';
};
redirect_uris = mkOption {
type = types.listOf types.str;
description = ''
List of redirect URIs that the client can use to redirect login attempts back to itself.
Consult the documentation for the other service if you aren't sure.
'';
};
allow_user_classes = mkOption {
type = types.listOf types.str;
description = ''
List of user classes which are authorised (allowed) to use this client (access this service).
As of idCoop v0.0.1, this setting is unimplemented and has no effect.
'';
};
};
};
in
{
options.services.idcoop = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable idCoop, a simple identity provider.
'';
};
configurePostgres = mkOption {
type = types.bool;
default = true;
description = ''
Whether to configure a Postgres database for idCoop.
Enabled by default.
'';
};
user = mkOption {
type = types.str;
default = defaultUser;
description = ''
User to run the service as.
Will be created automatically if it is left at the default.
'';
};
group = mkOption {
type = types.str;
default = defaultUser;
description = ''
User to run the service as.
Will be created automatically if it is left at the default.
'';
};
secretsPath = mkOption {
type = types.path;
default = builtins.toFile "blank.toml" "";
description = ''
Path to a file containing secrets. This file should be kept out of the Nix store.
Consult the idCoop documentation for the format of the secrets file.
'';
};
settings = mkOption {
default = {};
description = "idCoop configuration.";
type = types.submodule {
# freeformType = format.type; — one day we may want to enable freeform types, but for now just keep it strongly defined
options = {
listen = {
bind = mkOption {
type = types.str;
default = "127.0.0.1:8072";
description = ''
Host and port combination upon which to bind the web interface.
'';
};
public_base_uri = mkOption {
type = types.str;
default = "http://${cfg.settings.listen.bind}";
defaultText = "`http://{listen.bind}`";
description = ''
Public-facing HTTP(S) base URL.
'';
};
};
oidc = {
issuer = mkOption {
type = types.str;
default = cfg.settings.listen.public_base_uri;
defaultText = "`listen.public_base_uri`";
description = ''
The identity provider's 'issuer' identifier, as used in OpenID Connect.
This should be configured in clients (relying parties) and should likely not be changed.
'';
};
rsa_keypair = mkOption {
type = types.path;
description = ''
Path to an RSA keypair used for signing JSON Web Tokens.
'';
};
clients = mkOption {
type = types.attrsOf oidcClientSubmodule;
default = {};
description = ''
OpenID Connect 'clients' (also known as relying parties).
These entries are for the different services you want users to be able to log in to using idCoop.
'';
};
};
postgres = {
connect = mkOption {
type = types.str;
default = "postgres:";
description = mdDoc ''
Connection string for the Postgres database. The default of `postgres:` uses the [libpq environment variables] to form a connection;
usually this by default connects to the local UNIX socket with the current user's name as a username and database name,
if no environment variables are set.
[libpq environment variables]: https://www.postgresql.org/docs/current/libpq-envars.html
'';
};
};
};
};
};
};
config = {
users.users.idcoop = mkIf (cfg.enable && cfg.user == defaultUser) {
isSystemUser = true;
group = cfg.group;
home = mkDefault "/var/lib/idcoop";
createHome = true;
};
users.groups.idcoop = mkIf (cfg.enable && cfg.group == defaultUser) {};
systemd.services.idcoop = mkIf cfg.enable {
description = "idCoop: simple identity provider";
wantedBy = [ "multi-user.target" ];
after = [ "networking.target" "network-online.target" "postgresql.service" ];
serviceConfig =
let
configPath = writeTOML "idcoop_config.toml" cfg.settings;
in
{
ExecStart = "${idcoop}/bin/idcoop --config ${configPath} --secrets ${cfg.secretsPath} serve";
User = cfg.user;
Group = cfg.group;
};
};
services.postgresql = mkIf cfg.configurePostgres {
ensureUsers = [
{
name = "idcoop";
ensureDBOwnership = true;
}
];
ensureDatabases = ["idcoop"];
};
};
}

21
nixos_tests/lib.nix Normal file
View File

@ -0,0 +1,21 @@
# tests/lib.nix
# The first argument to this function is the test module itself
test:
# These arguments are provided by `flake.nix` on import, see checkArgs
{ pkgs, self}:
let
inherit (pkgs) lib;
# this imports the nixos library that contains our testing framework
nixos-lib = import (pkgs.path + "/nixos/lib") {};
in
(nixos-lib.runTest {
hostPkgs = pkgs;
# This speeds up the evaluation by skipping evaluating documentation (optional)
defaults.documentation.enable = lib.mkDefault false;
# This makes `self` available in the NixOS configuration of our virtual machines.
# This is useful for referencing modules or packages from your own flake
# as well as importing from other flakes.
node.specialArgs = { inherit self; };
imports = [ test ];
}).config.result

71
nixos_tests/starts.nix Normal file
View File

@ -0,0 +1,71 @@
let
rsaKeypair = ''
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDDu6acOa+3ae2S
0llp5oMsXjBMd5QJeQJCcY5Q9NAITF2U9VBwAiMf2wmaTZ1aWWFGSb/zWef7Hx1e
qNhsK9MYL+QdJih2I+KpMtDWm7hhy9FtCHVc9i1z9PruXb0om2jDWLuBkPdCqJZT
C58ObZKmgL4OH5F1Qv5JR/ZX21OjLolXPJo1sonLv9mlgufvhUmnC17onSSqFLBA
nhUedjbfdnLShkp0xa8G0nW7Ls7Idaxyo8M5S2M+azJyLI87eqjjfz0yIW0Am890
mRa81hO2D+YNcVA2wIE9MEI/ie480YxLQ0VHCjX4DcVir4ceExysYkdL8VK+U14g
NO67k4NVAgMBAAECggEABnseJyoZ0V7miOgCIemKClwMCVwkQLQLCRwtdCzG/p9Y
sef1g9/uPc3I4Z0USruO5v7mJi6h6cS7+jhpAhvpX3GmgfiTemXxyVxvYcvCLSrM
gmm3SR61npNMA7yC2OdcbqtvefjM1x4x7AoEeDvUkULOCDWvYUyYkuCZHYubl1mS
Rtcp9rxzky2tjdp8CHySBa9Kz9LEjWdFGky7g3vSyqZtw6tkK5CTwMPb9aHwiEq1
yDWCbqAPPnb300dXSqx7z3AcsxBi/lpCs79fQS1vSJ1/L9POpYxX0SMtffkR5+Bl
Mkg9dZUVenfbP4n40FdMypTTX2KJiMkc8+f0+Tz9QQKBgQDrdT7xs0lqMeZWp2rD
y2KLzRl0iO/+yKse+BgkeBggZ/Vh2TeF9ylTRYdmimxkJ8eZRipTr0F64BS/LPEk
RgWviuf9dUl0gyuhYTOJgUw1wcgtB6e+04UKEUsQW6JoNZekkM+xTa7FskLmlJ4J
zzowjF1lgJeEyX1tvWXKIVe+/QKBgQDUzy5nZoUqBrVTYxL2uadIUh8oiBKPU93U
Gz3DUq90yfDa7lFhwMRQRXfNqGUy6tshsaF4fT1b62hZDSz1OH3h/y1LKQOdF5kc
JJyk/4b7NJna16kwBLzWje5SjQKr51aQWU8JftZ5/8uck2j7vMi+mgwzpG45J7kv
Q1I5decBOQKBgFq1sKotB/uBfdukY91KXYy+VzAuEUd2x3YG3kYufhz97+riZCGY
NrN99cvrSBbNvHewMF5NBkzwRw3foob28vnN6dIbfVEFt6lUaSZwSYvsO9IdQOKj
Wn2ma+TBaK/89Y7QuzLzWoGPS3bJipj83M4XRWP1RmpBtbCxZqWYctWBAoGAMZPi
16wGsffGHpsiO+CcnDilkafByypar6N5DBwjTC4PsrF6vC9QjPLiKkNk8CvOyVa8
q3lh5hw9vyFWq/pxOUldn/j6Iorw3KGa7MWrCLMEdPtxKwKvi7ydHRZE3Q+UFyT3
SNsH1HxHTz74Yk1k5yK0XQOduisK9XvVmBVjr+ECgYEAyoSbo/1cyLKWgrIr0K/f
stiKL9SmBmYbaGaxtQToB5Hnqso7Hz5YEDlrcr8s1ukEFghgeNYuDYw3ZKKGGfZm
yVQKAt8ouoO8rfkLrtt0H+/0uJgouhewDEqf/O+MfzwDnFcT89J5ZTEf+9n6pjry
fuiQnuwEsPYGCCFuWWlrdHQ=
-----END PRIVATE KEY-----
'';
in
(import ./lib.nix) {
name = "idcoop-starts";
nodes = {
# `self` here is set by using specialArgs in `lib.nix`
node1 = { self, pkgs, ... }: {
imports = [ self.nixosModules.idcoop ];
environment.systemPackages = [ pkgs.curl pkgs.jq ];
services.idcoop = {
enable = true;
settings = {
oidc.rsa_keypair = builtins.toFile "rsa_keypair.pem" rsaKeypair;
# TODO for some reason the default doesn't work ???
postgres.connect = "postgres://%2Frun%2fpostgresql";
};
};
services.postgresql = {
enable = true;
enableTCPIP = false;
};
};
};
# This is the test code that will check if our service is running correctly:
testScript = ''
start_all()
# wait for our service to start
node1.wait_for_unit("idcoop")
# wait for the port to open
node1.wait_for_open_port(8072)
# check the OpenID Connect discovery is served
output = node1.succeed("curl http://localhost:8072/.well-known/openid-configuration | jq -e .")
'';
}

0
static/.gitkeep Normal file
View File

0
templates/.gitkeep Normal file
View File

0
translations/.gitkeep Normal file
View File