From 10b6b350bd072d44609878e1e8fc928f14ff6cef Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Tue, 21 May 2024 21:42:08 +0100 Subject: [PATCH] Add a Nix flake with NixOS module and test --- flake.lock | 81 +++++++++++++++++ flake.nix | 77 +++++++++++++++++ nixos_module.nix | 192 +++++++++++++++++++++++++++++++++++++++++ nixos_tests/lib.nix | 21 +++++ nixos_tests/starts.nix | 71 +++++++++++++++ static/.gitkeep | 0 templates/.gitkeep | 0 translations/.gitkeep | 0 8 files changed, 442 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 nixos_module.nix create mode 100644 nixos_tests/lib.nix create mode 100644 nixos_tests/starts.nix create mode 100644 static/.gitkeep create mode 100644 templates/.gitkeep create mode 100644 translations/.gitkeep diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..acf0e8d --- /dev/null +++ b/flake.lock @@ -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 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..d6cc3c7 --- /dev/null +++ b/flake.nix @@ -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; + }); + }); +} diff --git a/nixos_module.nix b/nixos_module.nix new file mode 100644 index 0000000..d4e2d5b --- /dev/null +++ b/nixos_module.nix @@ -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"]; + }; + }; +} diff --git a/nixos_tests/lib.nix b/nixos_tests/lib.nix new file mode 100644 index 0000000..cbdab44 --- /dev/null +++ b/nixos_tests/lib.nix @@ -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 + diff --git a/nixos_tests/starts.nix b/nixos_tests/starts.nix new file mode 100644 index 0000000..4abf594 --- /dev/null +++ b/nixos_tests/starts.nix @@ -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 .") + ''; +} + diff --git a/static/.gitkeep b/static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/templates/.gitkeep b/templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/translations/.gitkeep b/translations/.gitkeep new file mode 100644 index 0000000..e69de29