From 642f8e97935684518ed73555fb87a3133139441f Mon Sep 17 00:00:00 2001 From: ibizaman Date: Wed, 22 Feb 2023 23:04:44 -0800 Subject: [PATCH] [tests] add tests --- default.nix | 26 ++ tests/default.nix | 12 + tests/disnix/keycloak.nix | 93 +++++ tests/disnix/keycloak/distribution.nix | 7 + tests/disnix/keycloak/network.nix | 75 ++++ tests/disnix/keycloak/services.nix | 45 ++ tests/haproxy.nix | 549 +++++++++++++++++++++++++ tests/keycloak-cli-config.nix | 343 +++++++++++++++ tests/keycloak.nix | 232 +++++++++++ utils.nix | 88 ++++ 10 files changed, 1470 insertions(+) create mode 100644 default.nix create mode 100644 tests/default.nix create mode 100644 tests/disnix/keycloak.nix create mode 100644 tests/disnix/keycloak/distribution.nix create mode 100644 tests/disnix/keycloak/network.nix create mode 100644 tests/disnix/keycloak/services.nix create mode 100644 tests/haproxy.nix create mode 100644 tests/keycloak-cli-config.nix create mode 100644 tests/keycloak.nix create mode 100644 utils.nix diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..93f8286 --- /dev/null +++ b/default.nix @@ -0,0 +1,26 @@ +{ pkgs ? import {} +}: +let + utils = pkgs.callPackage ./utils.nix {}; +in +with builtins; +with pkgs.lib.attrsets; +with pkgs.lib.lists; +with pkgs.lib.strings; +rec { + tests = pkgs.callPackage ./tests { inherit utils; }; + + runtests = + let + onlytests = filterAttrs (name: value: name != "override" && name != "overrideDerivation") tests; + failingtests = filterAttrs (name: value: length value > 0) onlytests; + formatFailure = failure: toString failure; # TODO: make this more pretty + formattedFailureGroups = mapAttrsToList (name: failures: "${name}:\n${concatMapStringsSep "\n" formatFailure failures}") failingtests; + in + if length formattedFailureGroups == 0 then + "no failing test" + else + concatStringsSep "\n" formattedFailureGroups; + + disnixtests = pkgs.callPackage ./tests/disnix/keycloak.nix {}; +} diff --git a/tests/default.nix b/tests/default.nix new file mode 100644 index 0000000..da2cae0 --- /dev/null +++ b/tests/default.nix @@ -0,0 +1,12 @@ +# to run all tests: +# nix-instantiate --eval --strict . -A tests + +{ pkgs +, utils +}: + +{ + haproxy = pkgs.callPackage ./haproxy.nix { inherit utils; }; + keycloak = pkgs.callPackage ./keycloak.nix {}; + keycloak-cli-config = pkgs.callPackage ./keycloak-cli-config.nix {}; +} diff --git a/tests/disnix/keycloak.nix b/tests/disnix/keycloak.nix new file mode 100644 index 0000000..35845a9 --- /dev/null +++ b/tests/disnix/keycloak.nix @@ -0,0 +1,93 @@ +{ nixpkgs ? +, system ? builtins.currentSystem +}: + +let + pkgs = import nixpkgs {inherit system;}; + + disnixos = import "${pkgs.disnixos}/share/disnixos/testing.nix" { + inherit nixpkgs system; + }; + + version = "1.0"; +in + +rec { + tarball = disnixos.sourceTarball { + name = "testproject-zip"; + inherit version; + src = ./.; + officialRelease = false; + }; + + manifest = + disnixos.buildManifest { + name = "test-project-manifest"; + version = builtins.readFile ./version; + inherit tarball; + servicesFile = "keycloak/services.nix"; + networkFile = "keycloak/network.nix"; + distributionFile = "keycloak/distribution.nix"; + }; + + tests = + disnixos.disnixTest { + name = "test-project-tests"; + inherit tarball manifest; + networkFile = "keycloak/network.nix"; + dysnomiaStateDir = /var/state/dysnomia; + testScript = + '' + # Wait until the front-end application is deployed + $test1->waitForFile("/var/tomcat/webapps/testapp"); + + # Wait a little longer and capture the output of the entry page + my $result = $test1->mustSucceed("sleep 10; curl --fail http://test2:8080/testapp"); + ''; + }; +} + +# let +# utils = import ../../utils.nix { +# inherit pkgs; +# inherit (pkgs) stdenv lib; +# }; +# keycloak = import ../../pkgs/keycloak/unit.nix { +# inherit pkgs utils; +# inherit (pkgs) stdenv lib; +# }; +# in +# makeTest { +# nodes = { +# machine = {config, pkgs, ...}: +# { +# virtualisation.memorySize = 1024; +# virtualisation.diskSize = 4096; + +# environment.systemPackages = [ dysnomia pkgs.curl ]; +# }; +# }; +# testScript = '' +# def check_keycloak_activated(): +# machine.succeed("sleep 5") +# machine.succeed("curl --fail http://keycloak.test.tiserbox.com") + +# def check_keycloak_deactivated(): +# machine.succeed("sleep 5") +# machine.fail("curl --fail http://keycloak.test.tiserbox.com") + +# start_all() + +# # Test the keycloak module. Start keycloak and see if we can query the endpoint. +# machine.succeed( +# "dysnomia --type docker-container --operation activate --component ${keycloak} --environment" +# ) +# check_keycloak_activated() + +# # Deactivate keycloak. Check if it is not running anymore +# machine.succeed( +# "dysnomia --type docker-container --operation deactivate --component ${keycloak} --environment" +# ) +# check_keycloak_deactivated() +# ''; +# } diff --git a/tests/disnix/keycloak/distribution.nix b/tests/disnix/keycloak/distribution.nix new file mode 100644 index 0000000..24168c4 --- /dev/null +++ b/tests/disnix/keycloak/distribution.nix @@ -0,0 +1,7 @@ +{ infrastructure }: + +with infrastructure; +{ + KeycloakPostgresDB = [ test1 ]; + KeycloakService = [ test1 ]; +} diff --git a/tests/disnix/keycloak/network.nix b/tests/disnix/keycloak/network.nix new file mode 100644 index 0000000..92ba912 --- /dev/null +++ b/tests/disnix/keycloak/network.nix @@ -0,0 +1,75 @@ +rec { + test1 = { system + , pkgs + , lib + , ... }: + let + domain = "local"; + + utils = pkgs.lib.callPackageWith pkgs ../../../utils.nix { }; + + customPkgs = import ../../../pkgs/all-packages.nix { + inherit system pkgs utils; + }; + in + rec { + users.groups = { + keycloak = { + name = "keycloak"; + }; + }; + users.users = { + keycloak = { + name = "keycloak"; + group = "keycloak"; + isSystemUser = true; + }; + }; + + deployment.keys = { + keycloakinitialadmin.text = '' + KEYCLOAK_ADMIN_PASSWORD="${builtins.extraBuiltins.pass "keycloak.${domain}/admin"}" + ''; + }; + + services = { + openssh = { + enable = true; + }; + + disnix = { + enable = true; + # useWebServiceInterface = true; + }; + + postgresql = { + enable = true; + package = pkgs.postgresql_14; + + port = 5432; + enableTCPIP = true; + authentication = pkgs.lib.mkOverride 10 '' + local all all trust + host all all 127.0.0.1/32 trust + host all all ::1/128 trust + ''; + }; + }; + + dysnomia = { + enable = true; + enableLegacyModules = false; + extraContainerProperties = { + system = { + inherit domain; + }; + postgresql-database = { + service_name = "postgresql.service"; + port = builtins.toString services.postgresql.port; + }; + }; + }; + + networking.firewall.allowedTCPPorts = [ services.postgresql.port ]; + }; +} diff --git a/tests/disnix/keycloak/services.nix b/tests/disnix/keycloak/services.nix new file mode 100644 index 0000000..33de6ca --- /dev/null +++ b/tests/disnix/keycloak/services.nix @@ -0,0 +1,45 @@ +{ system, pkgs, distribution, invDistribution }: + +let + utils = pkgs.lib.callPackageWith pkgs ../../../utils.nix { }; + + customPkgs = import ../../../pkgs/all-packages.nix { + inherit system pkgs utils; + }; +in +{ + KeycloakPostgresDB = customPkgs.mkPostgresDB { + name = "KeycloakPostgresDB"; + database = "keycloak"; + username = "keycloak"; + # TODO: use passwordFile + password = "keycloak"; + }; + + KeycloakService = customPkgs.mkKeycloakService { + name = "KeycloakService"; + + # Get these from infrastructure.nix + user = "keycloak"; + group = "keycloak"; + + postgresServiceName = (utils.getTarget "KeycloakPostgresDB").containers.postgresql-database.service_name; + initialAdminUsername = "admin"; + + keys = { + dbPassword = "keycloakdbpassword"; + initialAdminPassword = "keycloakinitialadmin"; + }; + + logLevel = "INFO"; + hostname = "keycloak.${getDomain "KeycloakService"}"; + + dbType = "postgres"; + dbDatabase = KeycloakPostgresDB.database; + dbUsername = KeycloakPostgresDB.username; + dbHost = {KeycloakPostgresDB}: KeycloakPostgresDB.target.properties.hostname; + dbPort = (getTarget "KeycloakPostgresDB").containers.postgresql-database.port; + + inherit KeycloakPostgresDB; + }; +} diff --git a/tests/haproxy.nix b/tests/haproxy.nix new file mode 100644 index 0000000..41a73ec --- /dev/null +++ b/tests/haproxy.nix @@ -0,0 +1,549 @@ +# to run these tests: +# nix-instantiate --eval --strict . -A tests.haproxy + +{ lib +, stdenv +, pkgs +, utils +}: + +let + configcreator = pkgs.callPackage ./../haproxy/configcreator.nix { inherit utils; }; + mksiteconfig = pkgs.callPackage ./../haproxy/siteconfig.nix {}; + + diff = testResult: + with builtins; + with lib.strings; + if isString testResult.expected && isString testResult.result then + let + p = commonPrefixLength testResult.expected testResult.result; + s = commonSuffixLength testResult.expected testResult.result; + expectedSuffixLen = stringLength testResult.expected - s - p; + resultSuffixLen = stringLength testResult.result - s - p; + expectedDiff = substring p expectedSuffixLen testResult.expected; + resultDiff = substring p resultSuffixLen testResult.result; + omitted = len: if len == 0 then "" else "[... ${toString len} omitted]"; + in + {inherit (testResult) name; + commonPrefix = substring 0 p testResult.expected; + commonSuffix = substring (stringLength testResult.expected - s) s testResult.expected; + expected = "${omitted p}${expectedDiff}${omitted s}"; + result = "${omitted p}${resultDiff}${omitted s}"; + allExpected = testResult.expected; + allResult = testResult.result; + } + else testResult; + + runTests = x: map diff (lib.runTests x); +in + +with lib.attrsets; +runTests { + testDiffSame = { + expr = "abdef"; + expected = "abdef"; + }; + testUpdateByPath1 = { + expr = configcreator.updateByPath ["a"] (x: x+1) { + a = 1; + b = 1; + }; + expected = { + a = 2; + b = 1; + }; + }; + testUpdateByPath2 = { + expr = configcreator.updateByPath ["a" "a"] (x: x+1) { + a = { + a = 1; + b = 1; + }; + b = 1; + }; + expected = { + a = { + a = 2; + b = 1; + }; + b = 1; + }; + }; + testUpdateByPath3 = { + expr = configcreator.updateByPath ["a" "a" "a"] (x: x+1) { + a = { + a = { + a = 1; + b = 1; + }; + b = 1; + }; + b = 1; + }; + expected = { + a = { + a = { + a = 2; + b = 1; + }; + b = 1; + }; + b = 1; + }; + }; + + testRecursiveMerge1 = { + expr = configcreator.recursiveMerge [ + {a = 1;} + {b = 2;} + ]; + expected = { + a = 1; + b = 2; + }; + }; + + testRecursiveMerge2 = { + expr = configcreator.recursiveMerge [ + {a = {a = 1; b = 2;};} + {a = {a = 2;};} + ]; + expected = { + a = {a = 2; b = 2;}; + }; + }; + + tesFlattenArgs1 = { + expr = configcreator.flattenAttrs { + a = 1; + b = 2; + }; + expected = { + a = 1; + b = 2; + }; + }; + tesFlattenArgs2 = { + expr = configcreator.flattenAttrs { + a = { + a = 1; + b = { + c = 3; + d = 4; + }; + }; + b = 2; + }; + expected = { + "a.a" = 1; + "a.b.c" = 3; + "a.b.d" = 4; + b = 2; + }; + }; + + testHaproxyConfigDefaultRender = { + expr = configcreator.render (configcreator.default { + user = "me"; + group = "mygroup"; + certPath = "/cert/path"; + plugins = { + zone = { + luapaths = "lib"; + source = pkgs.writeText "one.lua" "a binary"; + }; + two = { + load = "right/two.lua"; + luapaths = "."; + cpaths = "right"; + source = pkgs.writeText "two.lua" "a binary"; + }; + }; + globalEnvs = { + ABC = "hello"; + }; + stats = null; + debug = false; + }); + expected = '' + global + group mygroup + log /dev/log local0 info + maxconn 20000 + lua-prepend-path /nix/store/ybcka9g095hp8s1hnm2ncfh1hp56v9yq-haproxyplugins/two/?.lua path + lua-prepend-path /nix/store/ybcka9g095hp8s1hnm2ncfh1hp56v9yq-haproxyplugins/two/right/?.so cpath + lua-prepend-path /nix/store/ybcka9g095hp8s1hnm2ncfh1hp56v9yq-haproxyplugins/zone/lib/?.lua path + lua-load /nix/store/ybcka9g095hp8s1hnm2ncfh1hp56v9yq-haproxyplugins/two/right/two.lua + setenv ABC hello + tune.ssl.default-dh-param 2048 + user me + + defaults + log global + option httplog + timeout client 15s + timeout connect 10s + timeout queue 100s + timeout server 30s + + frontend http-to-https + bind *:80 + mode http + redirect scheme https code 301 if !{ ssl_fc } + + frontend https + bind *:443 ssl crt /cert/path + http-request add-header X-Forwarded-Proto https + http-request set-header X-Forwarded-Port %[dst_port] + http-request set-header X-Forwarded-For %[src] + http-response set-header Strict-Transport-Security "max-age=15552000; includeSubDomains; preload;" + mode http + ''; + }; + + testHaproxyConfigDefaultRenderWithStatsAndDebug = { + expr = configcreator.render (configcreator.default { + user = "me"; + group = "mygroup"; + certPath = "/cert/path"; + stats = { + port = 8405; + uri = "/stats"; + refresh = "10s"; + prometheusUri = "/prom/etheus"; + hide-version = true; + }; + debug = true; + }); + expected = '' + global + group mygroup + log /dev/log local0 info + maxconn 20000 + tune.ssl.default-dh-param 2048 + user me + + defaults + log global + option httplog + timeout client 15s + timeout connect 10s + timeout queue 100s + timeout server 30s + + frontend http-to-https + bind *:80 + mode http + redirect scheme https code 301 if !{ ssl_fc } + + frontend https + bind *:443 ssl crt /cert/path + http-request add-header X-Forwarded-Proto https + http-request set-header X-Forwarded-Port %[dst_port] + http-request set-header X-Forwarded-For %[src] + http-response set-header Strict-Transport-Security "max-age=15552000; includeSubDomains; preload;" + log-format "%ci:%cp [%tr] %ft %b/%s %TR/%Tw/%Tc/%Tr/%Ta %ST %B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq %hr %hs %{+Q}r %sslv %sslc %[ssl_fc_cipherlist_str]" + mode http + + frontend stats + bind localhost:8405 + http-request use-service prometheus-exporter if { path /prom/etheus } + mode http + stats enable + stats hide-version + stats refresh 10s + stats uri /stats + ''; + }; + + testRenderHaproxyConfigWithSite = { + expr = configcreator.render (configcreator.default { + user = "me"; + group = "mygroup"; + certPath = "/cert/path"; + stats = null; + debug = false; + sites = { + siteName = { + frontend = { + capture = [ + "request header origin len 128" + ]; + acl = { + acl_siteName = "hdr_beg(host) siteName."; + acl_siteName_path = "path_beg /siteName"; + }; + http-response = { + add-header = [ + "Access-Control-Allow-Origin1 $[capture]" + "Access-Control-Allow-Origin2 $[capture]" + ]; + }; + use_backend = "if acl_siteName OR acl_siteName_path"; + }; + backend = { + servers = [ + { + name = "serviceName1"; + address = "serviceSocket"; + } + ]; + options = [ + "cookie JSESSIONID prefix" + ]; + }; + }; + }; + }); + expected = '' + global + group mygroup + log /dev/log local0 info + maxconn 20000 + tune.ssl.default-dh-param 2048 + user me + + defaults + log global + option httplog + timeout client 15s + timeout connect 10s + timeout queue 100s + timeout server 30s + + frontend http-to-https + bind *:80 + mode http + redirect scheme https code 301 if !{ ssl_fc } + + frontend https + acl acl_siteName hdr_beg(host) siteName. + acl acl_siteName_path path_beg /siteName + bind *:443 ssl crt /cert/path + capture request header origin len 128 + http-request add-header X-Forwarded-Proto https + http-request set-header X-Forwarded-Port %[dst_port] + http-request set-header X-Forwarded-For %[src] + http-response add-header Access-Control-Allow-Origin1 $[capture] + http-response add-header Access-Control-Allow-Origin2 $[capture] + http-response set-header Strict-Transport-Security "max-age=15552000; includeSubDomains; preload;" + mode http + use_backend siteName if acl_siteName OR acl_siteName_path + + backend siteName + cookie JSESSIONID prefix + mode http + option forwardfor + server serviceName1 serviceSocket + ''; + }; + + testRenderHaproxyConfigWith2Sites = { + expr = configcreator.render (configcreator.default { + user = "me"; + group = "mygroup"; + certPath = "/cert/path"; + stats = null; + debug = false; + sites = { + siteName = { + frontend = { + capture = [ + "request header origin len 128" + ]; + acl = { + acl_siteName = "hdr_beg(host) siteName."; + acl_siteName_path = "path_beg /siteName"; + }; + http-response = { + add-header = [ + "Access-Control-Allow-Origin1 $[capture]" + "Access-Control-Allow-Origin2 $[capture]" + ]; + }; + use_backend = "if acl_siteName OR acl_siteName_path"; + }; + backend = { + servers = [ + { + name = "serviceName1"; + address = "serviceSocket"; + } + ]; + options = [ + "cookie JSESSIONID prefix" + ]; + }; + }; + siteName2 = { + frontend = { + capture = [ + "request header origin len 128" + ]; + acl = { + acl_siteName2 = "hdr_beg(host) siteName2."; + acl_siteName2_path = "path_beg /siteName2"; + }; + http-response = { + add-header = [ + "Access-Control-Allow-Origin3 $[capture]" + "Access-Control-Allow-Origin4 $[capture]" + ]; + }; + use_backend = "if acl_siteName2 OR acl_siteName2_path"; + }; + backend = { + servers = [ + { + name = "serviceName2"; + address = "serviceSocket"; + } + ]; + options = [ + "cookie JSESSIONID prefix" + ]; + }; + }; + }; + }); + expected = '' + global + group mygroup + log /dev/log local0 info + maxconn 20000 + tune.ssl.default-dh-param 2048 + user me + + defaults + log global + option httplog + timeout client 15s + timeout connect 10s + timeout queue 100s + timeout server 30s + + frontend http-to-https + bind *:80 + mode http + redirect scheme https code 301 if !{ ssl_fc } + + frontend https + acl acl_siteName hdr_beg(host) siteName. + acl acl_siteName2 hdr_beg(host) siteName2. + acl acl_siteName2_path path_beg /siteName2 + acl acl_siteName_path path_beg /siteName + bind *:443 ssl crt /cert/path + capture request header origin len 128 + capture request header origin len 128 + http-request add-header X-Forwarded-Proto https + http-request set-header X-Forwarded-Port %[dst_port] + http-request set-header X-Forwarded-For %[src] + http-response add-header Access-Control-Allow-Origin1 $[capture] + http-response add-header Access-Control-Allow-Origin2 $[capture] + http-response add-header Access-Control-Allow-Origin3 $[capture] + http-response add-header Access-Control-Allow-Origin4 $[capture] + http-response set-header Strict-Transport-Security "max-age=15552000; includeSubDomains; preload;" + mode http + use_backend siteName if acl_siteName OR acl_siteName_path + use_backend siteName2 if acl_siteName2 OR acl_siteName2_path + + backend siteName + cookie JSESSIONID prefix + mode http + option forwardfor + server serviceName1 serviceSocket + + backend siteName2 + cookie JSESSIONID prefix + mode http + option forwardfor + server serviceName2 serviceSocket + ''; + }; + + testRenderHaproxyConfigWithSiteDebugHeaders = { + expr = configcreator.render (configcreator.default { + user = "me"; + group = "mygroup"; + certPath = "/cert/path"; + stats = null; + debug = false; + sites = { + siteName = { + frontend = { + capture = [ + "request header origin len 128" + ]; + acl = { + acl_siteName = "hdr_beg(host) siteName."; + acl_siteName_path = "path_beg /siteName"; + }; + http-response = { + add-header = [ + "Access-Control-Allow-Origin1 $[capture]" + "Access-Control-Allow-Origin2 $[capture]" + ]; + }; + use_backend = "if acl_siteName OR acl_siteName_path"; + }; + backend = { + servers = [ + { + name = "serviceName1"; + address = "serviceSocket"; + } + ]; + options = [ + "cookie JSESSIONID prefix" + ]; + }; + debugHeaders = "acl_siteName"; + }; + }; + }); + expected = '' + global + group mygroup + log /dev/log local0 info + maxconn 20000 + tune.ssl.default-dh-param 2048 + user me + + defaults + log global + option httplog + timeout client 15s + timeout connect 10s + timeout queue 100s + timeout server 30s + + frontend http-to-https + bind *:80 + mode http + redirect scheme https code 301 if !{ ssl_fc } + + frontend https + acl acl_siteName hdr_beg(host) siteName. + acl acl_siteName_path path_beg /siteName + bind *:443 ssl crt /cert/path + capture request header origin len 128 + http-request add-header X-Forwarded-Proto https + http-request capture req.hdrs len 512 if acl_siteName + http-request set-header X-Forwarded-Port %[dst_port] + http-request set-header X-Forwarded-For %[src] + http-response add-header Access-Control-Allow-Origin1 $[capture] + http-response add-header Access-Control-Allow-Origin2 $[capture] + http-response set-header Strict-Transport-Security "max-age=15552000; includeSubDomains; preload;" + log-format "%ci:%cp [%tr] %ft [[%hr]] %hs %{+Q}r" + mode http + option httplog + use_backend siteName if acl_siteName OR acl_siteName_path + + backend siteName + cookie JSESSIONID prefix + mode http + option forwardfor + server serviceName1 serviceSocket + ''; + }; +} diff --git a/tests/keycloak-cli-config.nix b/tests/keycloak-cli-config.nix new file mode 100644 index 0000000..2b22f60 --- /dev/null +++ b/tests/keycloak-cli-config.nix @@ -0,0 +1,343 @@ +# to run these tests: +# nix-instantiate --eval --strict . -A tests.keycloak-cli-config + +{ lib +, stdenv +, pkgs +}: + +let + configcreator = pkgs.callPackage ./../keycloak-cli-config/configcreator.nix { }; + + default_config = { + realm = "myrealm"; + domain = "mydomain.com"; + }; + + keep_fields = fields: + lib.filterAttrs (n: v: lib.any (n_: n_ == n) fields); +in + +lib.runTests { + testDefault = { + expr = configcreator default_config; + + expected = { + id = "myrealm"; + realm = "myrealm"; + enabled = true; + clients = []; + roles = { + realm = []; + client = {}; + }; + groups = []; + users = []; + }; + }; + + testUsers = { + expr = (configcreator (default_config // { + users = { + me = { + email = "me@mydomain.com"; + firstName = "me"; + lastName = "stillme"; + }; + }; + })).users; + + expected = [ + { + username = "me"; + enabled = true; + email = "me@mydomain.com"; + emailVerified = true; + firstName = "me"; + lastName = "stillme"; + } + ]; + }; + + testUsersWithGroups = { + expr = (configcreator (default_config // { + users = { + me = { + email = "me@mydomain.com"; + firstName = "me"; + lastName = "stillme"; + groups = [ "MyGroup" ]; + }; + }; + })).users; + + expected = [ + { + username = "me"; + enabled = true; + email = "me@mydomain.com"; + emailVerified = true; + firstName = "me"; + lastName = "stillme"; + groups = [ "MyGroup" ]; + } + ]; + }; + + testUsersWithRoles = { + expr = (configcreator (default_config // { + users = { + me = { + email = "me@mydomain.com"; + firstName = "me"; + lastName = "stillme"; + roles = [ "MyRole" ]; + }; + }; + })).users; + + expected = [ + { + username = "me"; + enabled = true; + email = "me@mydomain.com"; + emailVerified = true; + firstName = "me"; + lastName = "stillme"; + realmRoles = [ "MyRole" ]; + } + ]; + }; + + testUsersWithInitialPassword = { + expr = (configcreator (default_config // { + users = { + me = { + email = "me@mydomain.com"; + firstName = "me"; + lastName = "stillme"; + initialPassword = true; + }; + }; + })).users; + + expected = [ + { + username = "me"; + enabled = true; + email = "me@mydomain.com"; + emailVerified = true; + firstName = "me"; + lastName = "stillme"; + credentials = [ + { + type = "password"; + userLabel = "initial"; + value = "$(keycloak.users.me.password)"; + } + ]; + } + ]; + }; + + testGroups = { + expr = (configcreator (default_config // { + groups = [ "MyGroup" ]; + })).groups; + + expected = [ + { + name = "MyGroup"; + path = "/MyGroup"; + attributes = {}; + realmRoles = []; + clientRoles = {}; + subGroups = []; + } + ]; + }; + + testRealmRoles = { + expr = (configcreator (default_config // { + roles = { + A = [ "B" ]; + B = [ ]; + }; + })).roles; + + expected = { + client = {}; + realm = [ + { + name = "A"; + composite = true; + composites = { + realm = [ "B" ]; + }; + } + { + name = "B"; + composite = false; + } + ]; + }; + }; + + testClientRoles = { + expr = (configcreator (default_config // { + clients = { + clientA = { + roles = [ "cA" ]; + }; + }; + })).roles; + + expected = { + client = { + clientA = [ + { + name = "cA"; + clientRole = true; + } + ]; + }; + realm = []; + }; + }; + + testClient = { + expr = map (keep_fields [ + "clientId" + "rootUrl" + "redirectUris" + "webOrigins" + "authorizationSettings" + ]) (configcreator (default_config // { + clients = { + clientA = {}; + }; + })).clients; + expected = [ + { + clientId = "clientA"; + rootUrl = "https://clientA.mydomain.com"; + redirectUris = ["https://clientA.mydomain.com/oauth2/callback"]; + webOrigins = ["https://clientA.mydomain.com"]; + authorizationSettings = { + policyEnforcementMode = "ENFORCING"; + resources = []; + policies = []; + }; + } + ]; + }; + + testClientAuthorization = with builtins; { + expr = (head (configcreator (default_config // { + clients = { + clientA = { + resourcesUris = { + adminPath = ["/admin/*"]; + userPath = ["/*"]; + }; + access = { + admin = { + roles = [ "admin" ]; + resources = [ "adminPath" ]; + }; + user = { + roles = [ "user" ]; + resources = [ "userPath" ]; + }; + }; + }; + }; + })).clients).authorizationSettings; + expected = { + policyEnforcementMode = "ENFORCING"; + resources = [ + { + name = "adminPath"; + type = "urn:clientA:resources:adminPath"; + ownerManagedAccess = false; + uris = ["/admin/*"]; + } + { + name = "userPath"; + type = "urn:clientA:resources:userPath"; + ownerManagedAccess = false; + uris = ["/*"]; + } + ]; + policies = [ + { + name = "admin has access"; + type = "role"; + logic = "POSITIVE"; + decisionStrategy = "UNANIMOUS"; + config = { + roles = ''[{"id":"admin","required":true}]''; + }; + } + { + name = "user has access"; + type = "role"; + logic = "POSITIVE"; + decisionStrategy = "UNANIMOUS"; + config = { + roles = ''[{"id":"user","required":true}]''; + }; + } + { + name = "admin has access to adminPath"; + type = "resource"; + logic = "POSITIVE"; + decisionStrategy = "UNANIMOUS"; + config = { + resources = ''["adminPath"]''; + applyPolicies = ''["admin has access"]''; + }; + } + { + name = "user has access to userPath"; + type = "resource"; + logic = "POSITIVE"; + decisionStrategy = "UNANIMOUS"; + config = { + resources = ''["userPath"]''; + applyPolicies = ''["user has access"]''; + }; + } + ]; + }; + }; + + testClientAudience = + let + audienceProtocolMapper = config: + with builtins; + let + protocolMappers = (head config.clients).protocolMappers; + protocolMapperByName = name: protocolMappers: head (filter (x: x.name == name) protocolMappers); + in + protocolMapperByName "Audience" protocolMappers; + in + { + expr = audienceProtocolMapper (configcreator (default_config // { + clients = { + clientA = {}; + }; + })); + expected = { + name = "Audience"; + protocol = "openid-connect"; + protocolMapper = "oidc-audience-mapper"; + config = { + "included.client.audience" = "clientA"; + "id.token.claim" = "false"; + "access.token.claim" = "true"; + "included.custom.audience" = "clientA"; + }; + }; + }; +} diff --git a/tests/keycloak.nix b/tests/keycloak.nix new file mode 100644 index 0000000..7279922 --- /dev/null +++ b/tests/keycloak.nix @@ -0,0 +1,232 @@ +# to run these tests: +# nix-instantiate --eval --strict . -A tests.keycloak + +{ lib +, stdenv +, pkgs +}: + +let + configcreator = pkgs.callPackage ./../keycloak-cli-config/configcreator.nix {}; +in + +with lib.attrsets; +lib.runTests { + testConfigEmpty = { + expr = configcreator { + realm = "myrealm"; + domain = "domain.com"; + }; + expected = { + id = "myrealm"; + realm = "myrealm"; + enabled = true; + clients = []; + groups = []; + roles = { + client = {}; + realm = []; + }; + users = []; + }; + }; + + testConfigRole = { + expr = configcreator { + realm = "myrealm"; + domain = "domain.com"; + roles = { + user = []; + admin = ["user"]; + }; + }; + expected = { + id = "myrealm"; + realm = "myrealm"; + enabled = true; + clients = []; + groups = []; + roles = { + realm = [ + { + name = "admin"; + composite = true; + composites = { + realm = ["user"]; + }; + } + { + name = "user"; + composite = false; + } + ]; + client = {}; + }; + users = []; + }; + }; + + testConfigClient = { + expr = + let + c = configcreator { + realm = "myrealm"; + domain = "domain.com"; + clients = { + myclient = {}; + myclient2 = { + roles = ["uma"]; + }; + }; + }; + in + updateManyAttrsByPath [ + { + path = [ "clients" ]; + # We don't care about the value of the protocolMappers + # field because its value is hardcoded. + update = clients: map (filterAttrs (n: v: n != "protocolMappers")) clients; + } + ] c; + expected = { + id = "myrealm"; + realm = "myrealm"; + enabled = true; + clients = [ + { + clientId = "myclient"; + rootUrl = "https://myclient.domain.com"; + clientAuthenticatorType = "client-secret"; + redirectUris = [ + "https://myclient.domain.com/oauth2/callback" + ]; + webOrigins = [ + "https://myclient.domain.com" + ]; + authorizationServicesEnabled = true; + serviceAccountsEnabled = true; + protocol = "openid-connect"; + publicClient = false; + authorizationSettings = { + policyEnforcementMode = "ENFORCING"; + resources = []; + policies = []; + }; + } + { + clientId = "myclient2"; + rootUrl = "https://myclient2.domain.com"; + clientAuthenticatorType = "client-secret"; + redirectUris = [ + "https://myclient2.domain.com/oauth2/callback" + ]; + webOrigins = [ + "https://myclient2.domain.com" + ]; + authorizationServicesEnabled = true; + serviceAccountsEnabled = true; + protocol = "openid-connect"; + publicClient = false; + authorizationSettings = { + policyEnforcementMode = "ENFORCING"; + resources = []; + policies = []; + }; + } + ]; + groups = []; + roles = { + client = { + myclient = []; + myclient2 = [ + { + name = "uma"; + clientRole = true; + } + ]; + }; + realm = []; + }; + users = []; + }; + }; + + testConfigUser = { + expr = configcreator { + realm = "myrealm"; + domain = "domain.com"; + users = { + me = { + email = "me@me.com"; + firstName = null; + lastName = "Me"; + realmRoles = [ "role" ]; + }; + }; + }; + expected = { + id = "myrealm"; + realm = "myrealm"; + enabled = true; + clients = []; + groups = []; + roles = { + client = {}; + realm = []; + }; + users = [ + { + enabled = true; + username = "me"; + email = "me@me.com"; + emailVerified = true; + firstName = null; + lastName = "Me"; + } + ]; + }; + }; + + testConfigUserInitialPassword = { + expr = configcreator { + realm = "myrealm"; + domain = "domain.com"; + users = { + me = { + email = "me@me.com"; + firstName = null; + lastName = "Me"; + initialPassword = true; + }; + }; + }; + expected = { + id = "myrealm"; + realm = "myrealm"; + enabled = true; + clients = []; + groups = []; + roles = { + client = {}; + realm = []; + }; + users = [ + { + enabled = true; + username = "me"; + email = "me@me.com"; + emailVerified = true; + firstName = null; + lastName = "Me"; + credentials = [ + { + type = "password"; + userLabel = "initial"; + value = "$(keycloak.users.me.password)"; + } + ]; + } + ]; + }; + }; +} diff --git a/utils.nix b/utils.nix new file mode 100644 index 0000000..8b0306e --- /dev/null +++ b/utils.nix @@ -0,0 +1,88 @@ +{ stdenv +, pkgs +, lib +}: + +with lib; +with lib.lists; +with lib.attrsets; +rec { + tmpFilesFromDirectories = user: group: d: + let + wrapTmpfiles = dir: mode: "d '${dir}' ${mode} ${user} ${group} - -"; + in + mapAttrsToList wrapTmpfiles d; + + systemd = { + mkService = {name, content, timer ? null}: stdenv.mkDerivation { + inherit name; + + src = pkgs.writeTextDir "${name}.service" content; + timerSrc = pkgs.writeTextDir "${name}.timer" timer; + + installPhase = '' + mkdir -p $out/etc/systemd/system + cp $src/*.service $out/etc/systemd/system + '' + (if timer == null then "" else '' + cp $timerSrc/*.timer $out/etc/systemd/system + ''); + }; + + }; + + mkConfigFile = {dir, name, content}: stdenv.mkDerivation rec { + inherit name; + + src = pkgs.writeTextDir name content; + + buildCommand = '' + mkdir -p $out + cp ${src}/${name} $out/${name} + + echo "${dir}" > $out/.dysnomia-targetdir + + cat > $out/.dysnomia-fileset <